Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
#### Added

- Report all parsing errors in Markdown files (#389, @NathanReb)
- Add alternative syntax for explicitly setting the block-type.
The new label `type=...` can be set to `ocaml`, `toplevel`, `cram` or
`include`. (#385, @NathanReb)

#### Changed

Expand Down
2 changes: 1 addition & 1 deletion lib/block.ml
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ let infer_block ~loc ~config ~header ~contents ~errors =

let mk ~loc ~section ~labels ~legacy_labels ~header ~contents ~errors =
let block_kind =
get_label (function Block_kind x -> Some x | _ -> None) labels
get_label (function Block_type x -> Some x | _ -> None) labels
in
let config = get_block_config labels in
(match block_kind with
Expand Down
82 changes: 53 additions & 29 deletions lib/label.ml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,13 @@ type non_det = Nd_output | Nd_command

let default_non_det = Nd_output

type block_kind = OCaml | Cram | Toplevel | Include
type block_type = OCaml | Cram | Toplevel | Include

let pp_block_type ppf = function
| OCaml -> Fmt.string ppf "ocaml"
| Cram -> Fmt.string ppf "cram"
| Toplevel -> Fmt.string ppf "toplevel"
| Include -> Fmt.string ppf "include"

type t =
| Dir of string
Expand All @@ -84,13 +90,7 @@ type t =
| Version of Relation.t * Ocaml_version.t
| Set of string * string
| Unset of string
| Block_kind of block_kind

let pp_block_kind ppf = function
| OCaml -> Fmt.string ppf "ocaml"
| Cram -> Fmt.string ppf "cram"
| Toplevel -> Fmt.string ppf "toplevel"
| Include -> Fmt.string ppf "include"
| Block_type of block_type

let pp ppf = function
| Dir d -> Fmt.pf ppf "dir=%s" d
Expand All @@ -106,7 +106,7 @@ let pp ppf = function
Fmt.pf ppf "version%a%a" Relation.pp op Ocaml_version.pp v
| Set (v, x) -> Fmt.pf ppf "set-%s=%s" v x
| Unset x -> Fmt.pf ppf "unset-%s" x
| Block_kind bk -> pp_block_kind ppf bk
| Block_type bt -> Fmt.pf ppf "type=%a" pp_block_type bt

let is_prefix ~prefix s =
let len_prefix = String.length prefix in
Expand Down Expand Up @@ -140,41 +140,65 @@ let requires_value ~label ~value f =

let requires_eq_value ~label ~value f =
requires_value ~label ~value (fun op value ->
match op with Relation.Eq -> Ok (f value) | _ -> non_eq_op ~label)
match op with Relation.Eq -> f value | _ -> non_eq_op ~label)

let version_of_string s =
match Ocaml_version.of_string s with
| Ok v -> Ok v
| Error (`Msg e) -> Util.Result.errorf "Invalid version: %s." e

let parse_non_det_value ~label s =
match s with
| "output" -> Ok Nd_output
| "command" -> Ok Nd_command
| s ->
let allowed_values = [ "<none>"; {|"command"|}; {|"output"|} ] in
invalid_value ~label ~allowed_values s

let parse_block_type_value ~label s =
match s with
| "ocaml" -> Ok OCaml
| "cram" -> Ok Cram
| "toplevel" -> Ok Toplevel
| "include" -> Ok Include
| s ->
let allowed_values =
[ {|"ocaml"|}; {|"cram"|}; {|"toplevel"|}; {|"include"|} ]
in
invalid_value ~label ~allowed_values s

let interpret label value =
let open Util.Result.Infix in
match label with
| "skip" -> doesnt_accept_value ~label ~value Skip
| "ocaml" -> doesnt_accept_value ~label ~value (Block_kind OCaml)
| "cram" -> doesnt_accept_value ~label ~value (Block_kind Cram)
| "toplevel" -> doesnt_accept_value ~label ~value (Block_kind Toplevel)
| "include" -> doesnt_accept_value ~label ~value (Block_kind Include)
| "ocaml" -> doesnt_accept_value ~label ~value (Block_type OCaml)
| "cram" -> doesnt_accept_value ~label ~value (Block_type Cram)
| "toplevel" -> doesnt_accept_value ~label ~value (Block_type Toplevel)
| "include" -> doesnt_accept_value ~label ~value (Block_type Include)
| v when is_prefix ~prefix:"unset-" v ->
doesnt_accept_value ~label ~value
(Unset (split_prefix ~prefix:"unset-" v))
| "version" ->
requires_value ~label ~value (fun op v ->
match Ocaml_version.of_string v with
| Ok v -> Ok (Version (op, v))
| Error (`Msg e) ->
Util.Result.errorf "Invalid `version` label value: %s." e)
version_of_string v >>= fun v -> Ok (Version (op, v)))
| "non-deterministic" -> (
match value with
| None -> Ok (Non_det None)
| Some (Relation.Eq, "output") -> Ok (Non_det (Some Nd_output))
| Some (Relation.Eq, "command") -> Ok (Non_det (Some Nd_command))
| Some (Relation.Eq, v) ->
let allowed_values = [ "<none>"; {|"command"|}; {|"output"|} ] in
invalid_value ~label ~allowed_values v
| Some (Relation.Eq, s) ->
parse_non_det_value ~label s >>= fun nd -> Ok (Non_det (Some nd))
| Some _ -> non_eq_op ~label)
| "dir" -> requires_eq_value ~label ~value (fun x -> Dir x)
| "source-tree" -> requires_eq_value ~label ~value (fun x -> Source_tree x)
| "file" -> requires_eq_value ~label ~value (fun x -> File x)
| "part" -> requires_eq_value ~label ~value (fun x -> Part x)
| "env" -> requires_eq_value ~label ~value (fun x -> Env x)
| "dir" -> requires_eq_value ~label ~value (fun x -> Ok (Dir x))
| "source-tree" ->
requires_eq_value ~label ~value (fun x -> Ok (Source_tree x))
| "file" -> requires_eq_value ~label ~value (fun x -> Ok (File x))
| "part" -> requires_eq_value ~label ~value (fun x -> Ok (Part x))
| "env" -> requires_eq_value ~label ~value (fun x -> Ok (Env x))
| "type" ->
requires_eq_value ~label ~value (fun x ->
parse_block_type_value ~label x >>= fun bt -> Ok (Block_type bt))
| l when is_prefix ~prefix:"set-" l ->
requires_eq_value ~label ~value (fun x ->
Set (split_prefix ~prefix:"set-" l, x))
Ok (Set (split_prefix ~prefix:"set-" l, x)))
| l -> Error (`Msg (Format.sprintf "`%s` is not a valid label." l))

let of_string s =
Expand Down
4 changes: 2 additions & 2 deletions lib/label.mli
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type non_det = Nd_output | Nd_command

val default_non_det : non_det

type block_kind = OCaml | Cram | Toplevel | Include
type block_type = OCaml | Cram | Toplevel | Include

type t =
| Dir of string
Expand All @@ -42,7 +42,7 @@ type t =
| Version of Relation.t * Ocaml_version.t
| Set of string * string
| Unset of string
| Block_kind of block_kind
| Block_type of block_type

val pp : Format.formatter -> t -> unit

Expand Down
24 changes: 24 additions & 0 deletions test/bin/mdx-test/expect/block-type/test-case.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
It is possible to explicitly state the type of a block using the
`type` label to bypass the language header + content based inference,
providing better, more focused error messages.

The following blocks use a volontarily misleading language header that would
normally lead to errors if we let MDX infer the type of block based on them.

<!-- $MDX type=toplevel -->
```sh
# 1 + 1;;
```

<!-- $MDX type=ocaml -->
```sh
let x = 2
```

<!-- $MDX type=cram -->
```ocaml
$ echo "boom"
```

The include block type is somewhat redundant with the `file=...` label as
so it is not tested here.
26 changes: 26 additions & 0 deletions test/bin/mdx-test/expect/block-type/test-case.md.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
It is possible to explicitly state the type of a block using the
`type` label to bypass the language header + content based inference,
providing better, more focused error messages.

The following blocks use a volontarily misleading language header that would
normally lead to errors if we let MDX infer the type of block based on them.

<!-- $MDX type=toplevel -->
```ocaml
# 1 + 1;;
- : int = 2
```

<!-- $MDX type=ocaml -->
```ocaml
let x = 2
```

<!-- $MDX type=cram -->
```sh
$ echo "boom"
boom
```

The include block type is somewhat redundant with the `file=...` label as
so it is not tested here.
12 changes: 12 additions & 0 deletions test/bin/mdx-test/expect/dune.inc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
(alias runtest)
(action (diff bash-fence/test-case.md.expected bash-fence.actual)))

(rule
(target block-type.actual)
(deps (package mdx) (source_tree block-type))
(action
(with-stdout-to %{target}
(chdir block-type
(run ocaml-mdx test --output - test-case.md)))))

(rule
(alias runtest)
(action (diff block-type/test-case.md.expected block-type.actual)))

(rule
(target casual-file-inc.actual)
(deps (package mdx) (source_tree casual-file-inc))
Expand Down
14 changes: 14 additions & 0 deletions test/bin/mdx-test/failure/block-type-value/test-case.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
This tests that erros are properly reported when the `type` label
is misused.

It requires a value

<!-- $MDX type -->
```ocaml
```

It only accepts a fixed set of values

<!-- $MDX type=invalid -->
```ocaml
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[mdx] Fatal error: File "test-case.md", lines 6-8: invalid code block: Label `type` requires a value.
[mdx] Fatal error: File "test-case.md", lines 12-14: invalid code block: "invalid" is not a valid value for label `type`. Valid values are "ocaml", "cram", "toplevel" and "include".
26 changes: 26 additions & 0 deletions test/bin/mdx-test/failure/dune.inc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@
(alias runtest)
(action (diff block-locations/test-case.md.expected block-locations.actual)))

(rule
(target block-type-value.actual)
(deps (package mdx) (source_tree block-type-value))
(action
(with-accepted-exit-codes 1
(with-outputs-to %{target}
(chdir block-type-value
(run %{bin:ocaml-mdx} test test-case.md))))))

(rule
(alias runtest)
(action (diff block-type-value/test-case.md.expected block-type-value.actual)))

(rule
(target both-prelude.actual)
(deps (package mdx) (source_tree both-prelude))
Expand Down Expand Up @@ -65,6 +78,19 @@
(enabled_if (<> %{os_type} Win32))
(action (diff in-toplevel/test-case.md.expected in-toplevel.actual)))

(rule
(target include-without-file-label.actual)
(deps (package mdx) (source_tree include-without-file-label))
(action
(with-accepted-exit-codes 1
(with-outputs-to %{target}
(chdir include-without-file-label
(run %{bin:ocaml-mdx} test test-case.md))))))

(rule
(alias runtest)
(action (diff include-without-file-label/test-case.md.expected include-without-file-label.actual)))

(rule
(target invalid-label.actual)
(deps (package mdx) (source_tree invalid-label))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Explicitly setting the `type` to `include` has little benefits except
for warning you that the `file=...` label is mandatory.

<!-- $MDX type=include -->
```ocaml
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[mdx] Fatal error: File "test-case.md", lines 4-6: invalid code block: `file` label is required for include blocks.
4 changes: 2 additions & 2 deletions test/lib/test_block.ml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ let test_mk =
(test_name, `Quick, test_fun)
in
[
make_test ~name:"invalid ocaml" ~labels:[ Block_kind OCaml ]
make_test ~name:"invalid ocaml" ~labels:[ Block_type OCaml ]
~header:(Some OCaml) ~contents:[ "# let x = 2;;" ]
~expected:(Error (`Msg "toplevel syntax is not allowed in OCaml blocks."));
make_test ~name:"invalid toplevel" ~labels:[ Block_kind Toplevel ]
make_test ~name:"invalid toplevel" ~labels:[ Block_type Toplevel ]
~header:(Some OCaml) ~contents:[ "let x = 2;;" ]
~expected:(Error (`Msg "invalid toplevel syntax in toplevel blocks."));
]
Expand Down