My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Simplify @figure to alt-as-caption plain text

Odoc collapses newlines to spaces inside a paragraph and a blank line
terminates the @tag body, leaving no way to distinguish attributes
from a rich-text caption within one paragraph. Restrict v1 to:

@figure src=foo.png alt="Screenshot" link=https://…

The alt doubles as a plain-text figcaption. Rich captions with inline
formatting can follow in a v2 code-block variant. Updates the plan
to record the design constraint and decision.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+36 -38
+13 -9
docs/plans/2026-04-15-native-figures.md
··· 60 60 61 61 ## Design 62 62 63 - ### Syntax 63 + ### Syntax (v1 — shipped) 64 64 65 65 ``` 66 - @figure <src> [alt="…"] [link="…"] [class="…"] 67 - <caption block — any nestable block content> 66 + @figure <src> alt="…" [link="…"] [caption="…"] [class="…"] 68 67 ``` 69 68 70 - First line: the image source plus optional key=value attributes. 71 - Rest of the tag body (until next `@tag` or end of section): the 72 - caption. Attributes parsed with a tiny `key="value"` tokenizer. 69 + Everything after `@figure` is an attribute string. Bare first token is 70 + promoted to `src`. The `alt` attribute doubles as the figcaption unless 71 + a separate `caption="…"` attribute is given. Plain text only — no 72 + inline formatting in captions. 73 73 74 - Alternative: first paragraph of body is the caption; attributes pulled 75 - from a `key=value` word list. Slightly more forgiving but harder to 76 - write a parse error for. Going with explicit attrs on the tag line. 74 + **Why not a rich-text caption block?** Odoc's parser collapses 75 + newlines to spaces inside a paragraph, and a blank line terminates the 76 + `@tag` body. That leaves no way to distinguish "attrs" from "caption" 77 + within the same paragraph, and no way to attach a second paragraph to 78 + the tag. A v2 using a code-block extension (`{@figure[ rich caption 79 + with {{:url}links} ]}`) would work but defers richer captions. For 80 + now, plain-text alt-as-caption covers the existing blog use cases. 77 81 78 82 ### Rendering 79 83
+23 -29
odoc-jons-plugins/src/odoc_jons_plugins.ml
··· 673 673 let raw_block html = 674 674 Block.{ attr = []; desc = Raw_markup ("html", html) } 675 675 676 - (* Split the incoming body into (first paragraph text, remaining 677 - nestable blocks used as caption). *) 678 - let split_first_paragraph content = 679 - match content with 680 - | [] -> ("", []) 681 - | first :: rest -> 682 - let first_text = 683 - Api.text_of_nestable_block_elements [first] 684 - |> Stdlib.String.trim 685 - in 686 - (first_text, rest) 687 - 676 + (* v1 design: the entire tag body is treated as the attribute string. 677 + Syntax: [@figure src=foo.png alt="Caption text" link=https://…] 678 + The [alt] attribute doubles as the figcaption (plain text). For 679 + rich-text captions, fall back to raw HTML until a v2 exists. *) 688 680 let to_document ~tag:_ content = 689 - let attr_line, caption_blocks = split_first_paragraph content in 681 + let attr_line = 682 + Api.text_of_nestable_block_elements content 683 + |> Stdlib.String.trim 684 + in 690 685 let attrs = parse_attrs attr_line |> infer_src in 691 686 match find_attr "src" attrs with 692 687 | None -> ··· 695 690 Api.simple_output (Api.blocks_of_nestable_elements content) 696 691 | Some src -> 697 692 let alt = Option.value ~default:"" (find_attr "alt" attrs) in 693 + let caption = 694 + match find_attr "caption" attrs with 695 + | Some c -> c 696 + | None -> alt 697 + in 698 698 let link = find_attr "link" attrs in 699 699 let extra_class = Option.value ~default:"" (find_attr "class" attrs) in 700 - let open_tag = 701 - let cls = 702 - if extra_class = "" then "figure" 703 - else "figure " ^ extra_class 704 - in 705 - Printf.sprintf {|<figure class="%s">|} (escape cls) 700 + let cls = 701 + if extra_class = "" then "figure" 702 + else "figure " ^ extra_class 706 703 in 707 704 let img_html = 708 705 match link with ··· 713 710 Printf.sprintf {|<img src="%s" alt="%s">|} 714 711 (escape src) (escape alt) 715 712 in 716 - let caption_opening, caption_closing = 717 - if caption_blocks = [] then "", "" 718 - else {|<figcaption>|}, {|</figcaption>|} 713 + let caption_html = 714 + if caption = "" then "" 715 + else Printf.sprintf {|<figcaption>%s</figcaption>|} (escape caption) 719 716 in 720 - let blocks = 721 - [ raw_block (open_tag ^ img_html ^ caption_opening) ] 722 - @ (if caption_blocks = [] 723 - then [] 724 - else Api.blocks_of_nestable_elements caption_blocks) 725 - @ [ raw_block (caption_closing ^ "</figure>") ] 717 + let full_html = 718 + Printf.sprintf {|<figure class="%s">%s%s</figure>|} 719 + (escape cls) img_html caption_html 726 720 in 727 721 { 728 - Api.content = blocks; 722 + Api.content = [ raw_block full_html ]; 729 723 overrides = []; 730 724 resources = [ Api.Css_inline figure_css ]; 731 725 assets = [];