Skip to content

Commit ba41f92

Browse files
Merge pull request realworldocaml#357 from Leonidas-from-XIV/mli-metadata-labels
Recognize metadata labels in `mli` files
2 parents 4942357 + f397811 commit ba41f92

File tree

11 files changed

+220
-54
lines changed

11 files changed

+220
-54
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
#### Added
44

5+
- Add support for adding language tags and metadata labels in `mli` files.
6+
(#339, #357, @julow, @Leonidas-from-XIV)
7+
58
#### Changed
69

710
#### Deprecated

README.md

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,6 @@ and to practice "literate programming" using markdown and OCaml.
1313
The test mode allows to ensure that shell scripts and OCaml fragments
1414
in the documentation always stays up-to-date.
1515

16-
The blocks in markdown files can be parameterized by `mdx`-specific labels, that
17-
will change the way `mdx` interprets the block.
18-
The syntax is: `<!-- $MDX labels -->`, where `labels` is a list of valid labels
19-
separated by a comma. This line has to immediately precede the block it is
20-
attached to. Examples are given in the following sections.
21-
This syntax is the recommended way to define labels since `mdx` 1.7.0, to use the previous syntax please refer to the [mdx 1.6.0 README](https://github.com/realworldocaml/mdx/blob/1.6.0/README.md).
22-
2316
`mdx` is released as a single binary (called `ocaml-mdx`) and
2417
can be installed using opam:
2518

@@ -32,6 +25,33 @@ If you want to contribute or hack on the project, please see the
3225

3326
### Supported Extensions
3427

28+
#### Labels
29+
30+
The blocks in markdown files can be parameterized by `mdx`-specific labels, that
31+
will change the way `mdx` interprets the block.
32+
33+
The syntax is: `<!-- $MDX LABELS -->`, where `LABELS` is a list of valid labels
34+
separated by a comma. This line has to immediately precede the block it is
35+
attached to.
36+
37+
<!-- $MDX LABELS -->
38+
```ocaml
39+
```
40+
41+
This syntax is the recommended way to define labels since `mdx` 1.7.0, to use
42+
the previous syntax please refer to the
43+
[mdx 1.6.0 README](https://github.com/realworldocaml/mdx/blob/1.6.0/README.md).
44+
45+
It is also possible to use labels in OCaml interface files (`mli`), the syntax
46+
for this is is slightly different to match the conventions of OCaml
47+
documentation comments:
48+
49+
(** This is an documentation comment with an ocaml block
50+
{@ocaml LABELS [
51+
]}
52+
*)
53+
54+
3555
#### Shell Scripts
3656

3757
`ocaml-mdx` interprets shell scripts inside `sh` code blocks as cram-like tests. The

dune-project

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@
3535
result
3636
(ocaml-version
3737
(>= 2.3.0))
38-
(odoc-parser (>= 0.9.0))
38+
(odoc-parser (>= 1.0.0))
3939
(lwt :with-test)
4040
(alcotest :with-test)))

lib/block.ml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ let lstrip string =
131131

132132
let pp_contents ?syntax ppf t =
133133
match (syntax, t.contents) with
134-
| Some Syntax.Mli, [ _ ] -> Fmt.pf ppf "%s" (String.concat "\n" t.contents)
135-
| Some Syntax.Mli, _ ->
136-
Fmt.pf ppf "\n%a" (pp_lines syntax t) (List.map lstrip t.contents)
134+
| Some Syntax.Mli, [ line ] -> Fmt.pf ppf "%s" line
135+
| Some Syntax.Mli, lines ->
136+
Fmt.pf ppf "@\n%a@\n" (pp_lines syntax t) (List.map lstrip lines)
137137
| (Some Cram | Some Normal | None), [] -> ()
138138
| (Some Cram | Some Normal | None), _ ->
139139
Fmt.pf ppf "%a\n" (pp_lines syntax t) t.contents
@@ -146,10 +146,9 @@ let pp_errors ppf t =
146146
Fmt.string ppf "```\n"
147147
| _ -> ()
148148

149-
let pp_footer ?syntax ppf t =
149+
let pp_footer ?syntax ppf _ =
150150
match syntax with
151-
| Some Syntax.Mli ->
152-
if List.length t.contents = 1 then Fmt.pf ppf "" else Fmt.pf ppf "\n"
151+
| Some Syntax.Mli -> ()
153152
| Some Syntax.Cram -> ()
154153
| _ -> Fmt.string ppf "```\n"
155154

lib/cram.ml

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ module Log = (val Logs.src_log src : Logs.LOG)
2020
open Astring
2121
open Misc
2222

23-
type t = { command : string list; output : Output.t list; exit_code : int }
23+
type t = {
24+
command : string list;
25+
output : Output.t list;
26+
exit_code : int;
27+
vpad : int;
28+
}
2429

2530
let dump_line ppf = function
2631
| #Output.t as o -> Output.dump ppf o
@@ -37,10 +42,20 @@ let dump ppf (t : t) =
3742
Fmt.(Dump.list Output.dump)
3843
t.output t.exit_code
3944

45+
let pp_vpad ppf t =
46+
let rec loop = function
47+
| 0 -> ()
48+
| n ->
49+
Fmt.pf ppf "\n";
50+
loop (Int.pred n)
51+
in
52+
loop t.vpad
53+
4054
let pp_command ?(pad = 0) ppf (t : t) =
4155
match t.command with
4256
| [] -> ()
4357
| l ->
58+
pp_vpad ppf t;
4459
let sep ppf () = Fmt.pf ppf "\\\n%a> " pp_pad pad in
4560
Fmt.pf ppf "%a$ %a\n" pp_pad pad Fmt.(list ~sep string) l
4661

@@ -52,7 +67,7 @@ let pp ?pad ppf (t : t) =
5267
pp_lines (Output.pp ?pad) ppf t.output;
5368
pp_exit_code ?pad ppf t.exit_code
5469

55-
let pad_of_lines = function
70+
let hpad_of_lines = function
5671
| [] -> 0
5772
| h :: _ ->
5873
let i = ref 0 in
@@ -61,22 +76,29 @@ let pad_of_lines = function
6176
done;
6277
!i
6378

64-
let of_lines t =
65-
let pad = pad_of_lines t in
79+
let of_lines ~syntax ~(loc : Location.t) t =
80+
let pos = loc.loc_start in
81+
let hpad =
82+
match syntax with Syntax.Mli -> pos.pos_cnum + 2 | _ -> hpad_of_lines t
83+
in
6684
let unpad line =
67-
if String.is_empty line then line
68-
else if String.length line < pad then
69-
Fmt.failwith "invalid padding: %S" line
70-
else String.with_index_range line ~first:pad
85+
match syntax with
86+
| Syntax.Mli -> String.trim line
87+
| _ ->
88+
if String.is_empty line then line
89+
else if String.length line < hpad then
90+
Fmt.failwith "invalid padding: %S" line
91+
else String.with_index_range line ~first:hpad
7192
in
7293
let lines = List.map unpad t in
7394
let lines =
7495
Lexer_cram.token (Lexing.from_string (String.concat ~sep:"\n" lines))
7596
in
97+
let vpad = match syntax with Syntax.Mli -> 1 | _ -> 0 in
7698
Log.debug (fun l ->
77-
l "Cram.of_lines (pad=%d) %a" pad Fmt.(Dump.list dump_line) lines);
99+
l "Cram.of_lines (pad=%d) %a" hpad Fmt.(Dump.list dump_line) lines);
78100
let mk command output exit_code =
79-
{ command; output = List.rev output; exit_code }
101+
{ command; output = List.rev output; exit_code; vpad }
80102
in
81103
let rec command_cont acc = function
82104
| `Command_cont c :: t -> command_cont (c :: acc) t
@@ -101,8 +123,8 @@ let of_lines t =
101123
match lines with
102124
| `Command_first cmd :: t ->
103125
let cmd, t = command_cont [ cmd ] t in
104-
(pad, aux cmd [] [] t)
105-
| `Command cmd :: t -> (pad, aux [ cmd ] [] [] t)
126+
(hpad, aux cmd [] [] t)
127+
| `Command cmd :: t -> (hpad, aux [ cmd ] [] [] t)
106128
| [] -> (0, [])
107129
| `Output line :: _ ->
108130
if String.length line > 0 && line.[0] = '$' then

lib/cram.mli

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616

1717
(** Cram tests *)
1818

19-
type t = { command : string list; output : Output.t list; exit_code : int }
19+
type t = {
20+
command : string list;
21+
output : Output.t list;
22+
exit_code : int;
23+
vpad : int;
24+
}
2025
(** The type for cram tests. *)
2126

2227
(** {2 Accessors} *)
@@ -34,7 +39,7 @@ val command_line : t -> string
3439

3540
(** {2 Parser} *)
3641

37-
val of_lines : string list -> int * t list
42+
val of_lines : syntax:Syntax.t -> loc:Location.t -> string list -> int * t list
3843
(** [of_lines l] parses the commands [l]. It returns the optional
3944
whitespace padding. *)
4045

lib/mli_parser.ml

Lines changed: 70 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
open! Compat
2-
31
module Code_block = struct
4-
type t = { location : Odoc_parser.Loc.span; contents : string }
2+
type metadata = {
3+
language_tag : string Odoc_parser.Loc.with_location;
4+
labels : string Odoc_parser.Loc.with_location option;
5+
}
6+
7+
type t = {
8+
location : Odoc_parser.Loc.span;
9+
metadata : metadata option;
10+
contents : string;
11+
}
512
end
613

714
let drop_last lst =
@@ -51,8 +58,14 @@ let extract_code_blocks ~(location : Lexing.position) ~docstring =
5158
List.map
5259
(fun block ->
5360
match Odoc_parser.Loc.value block with
54-
| `Code_block (_metadata, { Odoc_parser.Loc.value = contents; _ }) ->
55-
[ { Code_block.location = block.location; contents } ]
61+
| `Code_block (metadata, { Odoc_parser.Loc.value = contents; _ }) ->
62+
let metadata =
63+
Option.map
64+
(fun (language_tag, labels) ->
65+
Code_block.{ language_tag; labels })
66+
metadata
67+
in
68+
[ { Code_block.location = block.location; metadata; contents } ]
5669
| `List (_, _, lists) -> List.map acc lists |> List.concat
5770
| _ -> [])
5871
blocks
@@ -118,6 +131,53 @@ let docstring_code_blocks str =
118131
(docstrings (Lexing.from_string str))
119132
|> List.concat
120133

134+
let make_block ~loc code_block =
135+
let handle_header = function
136+
| Some Code_block.{ language_tag; labels } -> (
137+
let header =
138+
Block.Header.of_string (Odoc_parser.Loc.value language_tag)
139+
in
140+
match labels with
141+
| None -> Ok (header, [])
142+
| Some labels -> (
143+
let labels = Odoc_parser.Loc.value labels |> String.trim in
144+
match Label.of_string labels with
145+
| Ok labels -> Ok (header, labels)
146+
| Error msgs -> Error (List.hd msgs)
147+
(* TODO: Report precise location *)))
148+
| None ->
149+
(* If not specified, blocks are run as ocaml blocks *)
150+
Ok (Some OCaml, [])
151+
in
152+
match handle_header code_block.Code_block.metadata with
153+
| Error _ as e -> e
154+
| Ok (header, labels) ->
155+
let contents = String.split_on_char '\n' code_block.contents in
156+
Block.mk ~loc ~section:None ~labels ~header ~contents ~legacy_labels:false
157+
~errors:[]
158+
159+
let code_block_markup code_block =
160+
let open Document in
161+
let opening =
162+
match code_block.Code_block.metadata with
163+
| Some { language_tag; labels } ->
164+
let labels =
165+
match labels with
166+
| Some s -> [ Text " "; Text (Odoc_parser.Loc.value s) ]
167+
| None -> []
168+
in
169+
[ Text "{@"; Text (Odoc_parser.Loc.value language_tag) ]
170+
@ labels @ [ Text "[" ]
171+
| None -> [ Text "{[" ]
172+
in
173+
let hpad =
174+
let has_several_lines = String.contains code_block.contents '\n' in
175+
let column = code_block.location.start.column in
176+
if not has_several_lines then ""
177+
else Astring.String.v ~len:column (fun _ -> ' ')
178+
in
179+
(opening, [ Text (hpad ^ "]}") ])
180+
121181
let parse_mli file_contents =
122182
(* Find the locations of the code blocks within [file_contents], then slice it up into
123183
[Text] and [Block] parts by using the starts and ends of those blocks as
@@ -132,22 +192,15 @@ let parse_mli file_contents =
132192
Document.Text
133193
(slice lines ~start:!cursor ~end_:code_block.location.start)
134194
in
135-
let column = code_block.location.start.column in
136-
let contents = String.split_on_char '\n' code_block.contents in
137195
let block =
138-
match
139-
Block.mk ~loc ~section:None ~labels:[] ~header:(Some OCaml)
140-
~contents ~legacy_labels:false ~errors:[]
141-
with
196+
match make_block ~loc code_block with
142197
| Ok block -> Document.Block block
143-
| Error _ -> failwith "Error creating block"
144-
in
145-
let hpad =
146-
if List.length contents = 1 then ""
147-
else Astring.String.v ~len:column (fun _ -> ' ')
198+
| Error (`Msg msg) ->
199+
failwith (Fmt.str "Error creating block: %s" msg)
148200
in
201+
let opening, closing = code_block_markup code_block in
149202
cursor := code_block.location.end_;
150-
[ pre_text; Text "{["; block; Text (hpad ^ "]}") ])
203+
[ pre_text ] @ opening @ [ block ] @ closing)
151204
code_blocks
152205
|> List.concat
153206
in

lib/test/mdx_test.ml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ let run_toplevel_tests ?syntax ?root c ppf tests t =
197197
Output.pp ~pad ppf (`Output line))
198198
output)
199199
tests;
200-
match syntax with Some Syntax.Mli -> () | _ -> Block.pp_footer ?syntax ppf t
200+
Block.pp_footer ?syntax ppf t
201201

202202
type file = { first : Mdx.Part.file; current : Mdx.Part.file }
203203

@@ -305,7 +305,11 @@ let run_exn ~non_deterministic ~silent_eval ~record_backtrace ~syntax ~silent
305305
with_non_det non_deterministic non_det ~command:print_block
306306
~output:det ~det
307307
| Cram { language = _; non_det } ->
308-
let pad, tests = Cram.of_lines t.contents in
308+
let pad, tests =
309+
Cram.of_lines
310+
~syntax:(Option.value ~default:Normal syntax)
311+
~loc:t.loc t.contents
312+
in
309313
with_non_det non_deterministic non_det ~command:print_block
310314
~output:(fun () ->
311315
print_block ();

mdx.opam

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ depends: [
3030
"re" {>= "1.7.2"}
3131
"result"
3232
"ocaml-version" {>= "2.3.0"}
33-
"odoc-parser" {>= "0.9.0"}
33+
"odoc-parser" {>= "1.0.0"}
3434
"lwt" {with-test}
3535
"alcotest" {with-test}
3636
"odoc" {with-doc}

test/bin/mdx-test/expect/simple-mli/test-case.mli

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,33 @@
1919
print_endline (String.concat " " [the; last; phrase])
2020
]}
2121
22-
{[
22+
With the optional header:
23+
24+
{@ocaml[
2325
# List.map (fun x -> x * x) [(1 + 9); 2; 3];;
2426
- : int list = [100; 4; 9]
2527
# List.map (fun x -> x * x) [1; 2; 3];;
2628
- : int list = [1; 4; 9]
2729
]}
30+
31+
A shell block:
32+
33+
{@sh set-FOO=Hello,set-BAR=Bash[
34+
$ echo $FOO $BAR
35+
Hello Bash
36+
]}
37+
38+
A block that doesn't run:
39+
40+
{@text[
41+
# 1
42+
= 2 ?
43+
]}
2844
*)
2945
val foo : string
3046

31-
(** {[1 + 1 = 3]} *)
47+
(** {@ocaml[1 + 1 = 3]} *)
3248
val bar : string
49+
50+
(** {@ocaml skip[1 + 1 = 3]} *)
51+
val baz : string

0 commit comments

Comments
 (0)