From a1f7ca74033c22c34359c3e6464c46d3eb8c4dfc Mon Sep 17 00:00:00 2001 From: Tim-ats-d Date: Mon, 20 Jul 2026 15:55:06 +0200 Subject: [PATCH 1/4] Add support for multiline links (issue #865). --- CHANGES.md | 4 +++ src/parser/syntax.ml | 29 +++++++++++++++++-- .../model/semantics/expected/heading.expected | 7 +++++ test/model/semantics/odoc_semantic_test.ml | 7 +++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 551d12f499..7771ef3991 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,14 @@ # Unreleased +### Added - Support for OxCaml unboxed named types (@art-w, #1407) - Support for OxCaml zero alloc definitions (@Leonidas-from-XIV, #1422, #1444) - Remove requirement for ppx_expect in tests (@jonludlam, #1445) - Support for OxCaml modalities (@art-w, #1420) +### Fixed +- Allow to break link into multiline (@Tim-ats-d, #1439) + # 3.2.1 ### Fixed diff --git a/src/parser/syntax.ml b/src/parser/syntax.ml index 454aa20c9d..ff132f1ebd 100644 --- a/src/parser/syntax.ml +++ b/src/parser/syntax.ml @@ -1,3 +1,4 @@ + (* This module is a recursive descent parser for the ocamldoc syntax. The parser consumes a token stream of type [Token.t Stream.t], provided by the lexer, and produces a comment AST of the type defined in [Parser_.Ast]. @@ -167,6 +168,30 @@ type token_that_always_begins_an_inline_element = | `Begin_link_with_replacement_text of string | `Math_span of string ] +let escape_link link = + let link = String.trim link in + let buf = Buffer.create (String.length link) in + let last_state = + String.fold_left + (fun state chr -> + match (state, chr) with + | `Char, '\\' -> `Backslash + | `Char, _ -> + Buffer.add_char buf chr; + `Char + | (`Backslash | `Escaping), (' ' | '\t' | '\n') -> `Escaping + | (`Backslash | `Escaping), _ -> + Buffer.add_char buf chr; + `Char) + `Char link + in + let () = + match last_state with + | `Backslash -> Buffer.add_char buf '\\' + | `Escaping | `Char -> () + in + Buffer.contents buf + (* Check that the token constructors above actually are all in [Token.t]. *) let _check_subset : token_that_always_begins_an_inline_element -> Token.t = fun t -> (t :> Token.t) @@ -269,7 +294,7 @@ let rec inline_element : | `Simple_link u -> junk input; - let u = String.trim u in + let u = escape_link u |> String.trim in if u = "" then Parse_error.should_not_be_empty @@ -281,7 +306,7 @@ let rec inline_element : | `Begin_link_with_replacement_text u as parent_markup -> junk input; - let u = String.trim u in + let u = escape_link u |> String.trim in if u = "" then Parse_error.should_not_be_empty diff --git a/test/model/semantics/expected/heading.expected b/test/model/semantics/expected/heading.expected index a5659c0810..205519bd6e 100644 --- a/test/model/semantics/expected/heading.expected +++ b/test/model/semantics/expected/heading.expected @@ -174,6 +174,13 @@ foo --- output --- {"value":[{"`Heading":[{"heading_level":"`Subsection","heading_label_explicit":"false"},{"`Label":[{"`Page":["None","f.ml"]},""]},[{"`Link":["foo",[]]}]]}],"warnings":[]} --- input --- +{{:https://github.com/ocaml/\ + odoc/\ +issues/\ + 865\ }this issue} +--- output --- +{"value":[{"`Paragraph":[{"`Link":["https://github.com/ocaml/odoc/issues/865\\",[{"`Word":"this"},"`Space",{"`Word":"issue"}]]}]}],"warnings":["File \"f.ml.mld\":\nPages (.mld files) should start with a heading."]} +--- input --- {2 {!foo}} --- output --- {"value":[{"`Heading":[{"heading_level":"`Subsection","heading_label_explicit":"false"},{"`Label":[{"`Page":["None","f.ml"]},""]},[{"`Reference":[{"`Root":["foo","`TUnknown"]},[]]}]]}],"warnings":[]} diff --git a/test/model/semantics/odoc_semantic_test.ml b/test/model/semantics/odoc_semantic_test.ml index 7758c8b7bf..0931384a2a 100644 --- a/test/model/semantics/odoc_semantic_test.ml +++ b/test/model/semantics/odoc_semantic_test.ml @@ -264,6 +264,13 @@ let heading () = let link_in_markup = test "{2 {{:foo}}}" + let multilines_link_in_markup = + test + {|{{:https://github.com/ocaml/\ + odoc/\ +issues/\ + 865\ }this issue}|} + let reference_in_markup = test "{2 {!foo}}" let two = test "{2 Foo}\n{2 Bar}" From 44fe33f0e8ed0204fd22026b52ffc8485a325994 Mon Sep 17 00:00:00 2001 From: Tim-ats-d Date: Tue, 21 Jul 2026 14:52:08 +0200 Subject: [PATCH 2/4] Adress Paul-Elliot's feedback. --- doc/cheatsheet.mld | 10 ++++++++-- doc/odoc_for_authors.mld | 12 ++++++++++-- src/parser/syntax.ml | 23 +++++++++++------------ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/doc/cheatsheet.mld b/doc/cheatsheet.mld index 62eeeda26d..4233a85333 100644 --- a/doc/cheatsheet.mld +++ b/doc/cheatsheet.mld @@ -57,11 +57,17 @@ Quick reference for the odoc language! {@text[ Here is a link: {:https://www.example.com}. - You can also click {{:https://www.example.com}here}.]}} + You can also click {{:https://www.example.com}here}. + + Multi lines links are also supported: {:https://www.example.com/\ + a-very\ + long-path/}]}} {td Here is a link: {:https://www.example.com}. - You can also click {{:https://www.example.com}here}.}} + You can also click {{:https://www.example.com}here}. + + Multi lines links are also supported: {:https://www.example.com/a-very-long-path/}.}} {tr {th {{!odoc_for_authors.links_and_references}References} } {td diff --git a/doc/odoc_for_authors.mld b/doc/odoc_for_authors.mld index c2b558ae4a..9f10727e62 100644 --- a/doc/odoc_for_authors.mld +++ b/doc/odoc_for_authors.mld @@ -545,9 +545,17 @@ A link to a URL may be put into the text as follows: (** See {{: https://ocaml.org/ }the OCaml website} for news about OCaml *) ]} -This will render as a link to [https://ocaml.org/] with the text "the OCaml website" +This will render as a link to [https://ocaml.org/] with the text "the OCaml website". -References are links to other elements, e.g., comments might wish to refer to +Link can also be spread accross lines: +{[ +{{: https://github.com/ocaml/\ + odoc/issues/ }Issues opened on Odoc} +]} + +Use a backslash to mark line breaks. When a backslash is followed by whitespace, both are removed. + +References are links to other elements, e.g., comments might wish to refer to a module or type elsewhere as follows: {[ diff --git a/src/parser/syntax.ml b/src/parser/syntax.ml index ff132f1ebd..3839224ab7 100644 --- a/src/parser/syntax.ml +++ b/src/parser/syntax.ml @@ -1,4 +1,3 @@ - (* This module is a recursive descent parser for the ocamldoc syntax. The parser consumes a token stream of type [Token.t Stream.t], provided by the lexer, and produces a comment AST of the type defined in [Parser_.Ast]. @@ -173,17 +172,17 @@ let escape_link link = let buf = Buffer.create (String.length link) in let last_state = String.fold_left - (fun state chr -> - match (state, chr) with - | `Char, '\\' -> `Backslash - | `Char, _ -> - Buffer.add_char buf chr; - `Char - | (`Backslash | `Escaping), (' ' | '\t' | '\n') -> `Escaping - | (`Backslash | `Escaping), _ -> - Buffer.add_char buf chr; - `Char) - `Char link + (fun state chr -> + match (state, chr) with + | `Char, '\\' -> `Backslash + | `Char, _ -> + Buffer.add_char buf chr; + `Char + | (`Backslash | `Escaping), _ when Char.Ascii.is_white chr -> `Escaping + | (`Backslash | `Escaping), _ -> + Buffer.add_char buf chr; + `Char) + `Char link in let () = match last_state with From 5988d9768f7e8b2dfb1d630a4b2d77c451fb7ec1 Mon Sep 17 00:00:00 2001 From: Tim-ats-d Date: Wed, 22 Jul 2026 12:21:02 +0200 Subject: [PATCH 3/4] Factorize backslash adding. --- src/parser/syntax.ml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/parser/syntax.ml b/src/parser/syntax.ml index 3839224ab7..0961490d81 100644 --- a/src/parser/syntax.ml +++ b/src/parser/syntax.ml @@ -170,6 +170,10 @@ type token_that_always_begins_an_inline_element = let escape_link link = let link = String.trim link in let buf = Buffer.create (String.length link) in + let add_backslash = function + | `Backslash -> Buffer.add_char buf '\\' + | `Escaping | `Char -> () + in let last_state = String.fold_left (fun state chr -> @@ -179,16 +183,13 @@ let escape_link link = Buffer.add_char buf chr; `Char | (`Backslash | `Escaping), _ when Char.Ascii.is_white chr -> `Escaping - | (`Backslash | `Escaping), _ -> + | ((`Backslash | `Escaping) as state), _ -> + add_backslash state; Buffer.add_char buf chr; `Char) `Char link in - let () = - match last_state with - | `Backslash -> Buffer.add_char buf '\\' - | `Escaping | `Char -> () - in + add_backslash last_state; Buffer.contents buf (* Check that the token constructors above actually are all in [Token.t]. *) From f8ce19180da6d50d942be03eac00e0bcc2a93036 Mon Sep 17 00:00:00 2001 From: Paul-Elliot Date: Wed, 22 Jul 2026 12:42:00 +0200 Subject: [PATCH 4/4] Add a comment about what the function does --- src/parser/syntax.ml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/parser/syntax.ml b/src/parser/syntax.ml index 0961490d81..0998e4c648 100644 --- a/src/parser/syntax.ml +++ b/src/parser/syntax.ml @@ -167,6 +167,8 @@ type token_that_always_begins_an_inline_element = | `Begin_link_with_replacement_text of string | `Math_span of string ] +(** When a backslash is followed by space, remove both the backslash and the + space. *) let escape_link link = let link = String.trim link in let buf = Buffer.create (String.length link) in