package mdx

  1. Overview
  2. Docs
Executable code blocks inside markdown files

Install

Dune Dependency

Authors

Maintainers

Sources

mdx-2.1.0.tbz
sha256=a25d73cbc2ce0c361d727bff8c1c84d299ea279ba342e3002eadeff72eed77fa
sha512=05e86a3a4513299433583b8c7cd482edfc513b08002825ae8ec390e537139eb7dd7b9131679d181e62147f9ff0d1814a1936d2e2983fc7530dcb85e347d4b603

Description

ocaml-mdx allows to execute code blocks inside markdown files. There are (currently) two sub-commands, corresponding to two modes of operations: pre-processing (ocaml-mdx pp) and tests (ocaml-mdx test).

The pre-processor mode allows to mix documentation and code, and to practice "literate programming" using markdown and OCaml.

The test mode allows to ensure that shell scripts and OCaml fragments in the documentation always stays up-to-date.

Published: 28 Jan 2022

README

README.md

mdx -- executable code blocks inside markdown files

mdx allows to execute code blocks inside markdown files. There are (currently) two sub-commands, corresponding to two modes of operations: pre-processing (ocaml-mdx pp) and tests (ocaml-mdx test).

The pre-processor mode allows to mix documentation and code, and to practice "literate programming" using markdown and OCaml.

The test mode allows to ensure that shell scripts and OCaml fragments in the documentation always stays up-to-date.

mdx is released as a single binary (called ocaml-mdx) and can be installed using opam:

$ opam install mdx

If you want to contribute or hack on the project, please see the CONTRIBUTING.md.

Supported Extensions

Labels

The blocks in markdown files can be parameterized by mdx-specific labels, that will change the way mdx interprets the block.

The syntax is: <!-- $MDX LABELS -->, where LABELS is a list of valid labels separated by a comma. This line has to immediately precede the block it is attached to.

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

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.

It is also possible to use labels in OCaml interface files (mli), the syntax for this is is slightly different to match the conventions of OCaml documentation comments:

(** This is an documentation comment with an ocaml block
    {@ocaml LABELS [
    ]}
*)
Shell Scripts

ocaml-mdx interprets shell scripts inside sh code blocks as cram-like tests. The syntax is the following:

  • Lines beginning with a dollar sign and a space are commands and will be run in the shell.

  • Multi-lines commands end by \ and continue with two spaces and a > sign on the next line:

     ```sh
     $ <line1> \
     > <line2> \
     > <line3>
     ```
    
  • Commands support the heredoc syntax (<<):

     ```sh
     $ cat <<EOF \
     > hello\
     > world\
     > EOF
     hello
     world
     ```
    
  • Lines beginning without a dollar sign are considered command outputs.

  • Command outputs can contain ellipses: .... These will match any possible outputs (on zero, one or multiple lines).

  • Arbitrary padding with whitespace is supported, as long as it is consistent inside a code block.

Here is an example of a markdown file using shell scripts inside code blocks, with a padding of 3:

```sh
   $ for i in `seq 1 10`
   1
   ...
   10
```

ocaml-mdx will also consider exit codes when the syntax [<exit code>]is used:

```sh
$ exit 1
[1]
```

Note that nothing will be displayed when the exit code is 0 (e.g. in case of success).

OCaml Code

ocaml-mdx interprets OCaml fragments. It understands normal code fragments and toplevel code fragments (starting with a # sign and optionally ending with ;;). Arbitrary whitespace padding is supported, at long as it stays consistent within a code block.

Toplevel fragments interleave OCaml code and their corresponding outputs.

Here is an example of normal OCaml code:

```ocaml
print_endline "42"
```

Here is an examples of toplevel OCaml code:

```ocaml
# print_endline "42"
42
```

File sync

mdx is also capable of including content from files in fenced code blocks using the label file. OCaml files can be sliced using named blocks:

(* $MDX part-begin=partName *)
let meaning_of_life () =
  print_endline "42"
(* $MDX part-end *)

These can then be included in the document:

<!-- $MDX file=sync_to_md.ml,part=partName -->
```ocaml
```

Non-OCaml files can also be read and included in a block:

<!-- $MDX file=any_file.txt -->
```
```

However, part splitting is only supported for OCaml files.

Pre-processing

ocaml-mdx pp allows to transform a markdown file into a valid OCaml file, which can be passed to OCaml using the -pp option.

For instance, given the following file.md document:

```ocaml
# print_endline "42"
42
```

Can be compiled and executed using:

$ ocamlc -pp 'ocaml-mdx pp' -impl file.md -o file.exe
$ ./file.exe
42

This can be automated using dune:

(rule
 ((targets (file.ml))
  (deps    (file.md))
  (action  (with-stdout-to ${@} (run ocaml-mdx pp ${<})))))

(executable ((name file)))

Tests

Cram Tests

Cram tests can be executed and checked with ocaml-mdx test <file.md>.

```sh
 $ for i in `seq 1 10`; do echo $i; done
 1
 ...
 10
 ```

If the output is not consistent with what is expected, <file.md>.corrected is generated.

OCaml

To execute OCaml code and toplevel fragments, uses ocaml-mdx test <file.md>.

```ocaml
# print_endline "42"
42
```

If the output is not consistent with what is expected <file.md>.corrected is generated.

Integration with Dune

To test that the code blocks of file.md stay consistent, one can use dune's mdx stanza:

(mdx
 (files file.md))

This allows to test the consistency of a markdown file using the normal dev workflow:

$ dune runtest

will display a diff of the output if something has changed. For instance:

$ dune runtest
------ file.md
++++++ file.md.corrected
File "file.md", line 23, characters 0-1:
 |
 |```sh
-| $ for i in `seq 1 3`; do echo $i; done
+| $ for i in `seq 1 4`; do echo $i; done
 | 1
 | 2
 | 3
+| 4
 |```

And the changes can then be accepted using:

$ dune promote

For further details about the mdx stanza you should read the according section in the dune documentation.

Non-deterministic Tests

Non-deterministic Outputs

ocaml-mdx test supports non-deterministic outputs:

<!-- $MDX non-deterministic=output -->
```sh
$ <command>
<output>
```

In that case, ppx test <file> will run the command but will not generate <file>.corrected if the new output differs from the one described in the file. Use ocaml-mdx test --non-deterministic <file> to come back to the default behaviour.

Non-deterministic Commands

ocaml-mdx test supports non-deterministic commands:

<!-- $MDX non-deterministic=command -->
```ocaml
# Random.int 10;;
- : int = 5
```

In that case, ocaml-mdx test <file> will not run the command. Use ocaml-mdx test --non-deterministic <file> to come back to the default behaviour.

Named execution environments (since mdx 1.1.0)

Separate environments can be defined for blocks:

x holds the value 1 in the environment e1.

<!-- $MDX env=e1 -->
```ocaml
let x = 1;;
```

<!-- $MDX env=e1 -->
```ocaml
module M = struct let k = 42 let f x = x * k end;;
```

x holds the value 3 in the environment e2.

<!-- $MDX env=e2 -->
```ocaml
let x = 3;;
```

We can retrieve the value of x in environment e1:

<!-- $MDX env=e1 -->
```ocaml
# print_int x;;
1
- : unit = ()
# print_int M.k;;
42
- : unit = ()
# M.f;;
- : int -> int = <fun>
```
Matching on the OCaml version (since mdx 1.2.0)

Blocks can be processed or ignored depending on the current version of OCaml.

For example to have a different outcome whether we are past OCaml 4.06:

<!-- $MDX version<4.06 -->
```ocaml
# let f x = x + 1
val f : int -> int = <fun>
# let f y =
  y^"foo"
val f : bytes -> bytes = <fun>
```

<!-- $MDX version>=4.06 -->
```ocaml
# let f x = x + 1
val f : int -> int = <fun>
# let f y =
  y^"foo"
val f : string -> string = <fun>
```

The available operators are <>, >=, >, <=, < and =. The version number can be of the following forms:

  • *

  • X

  • X.Y

  • X.Y.Z

Environment variables declaration

Environment variables can be declared at the beginning of a block:

<!-- $MDX set-FOO=bar,set-BAR=foo -->
```ocaml
# print_endline (Sys.getenv "FOO")
bar
- : unit = ()
# print_endline (Sys.getenv "BAR")
foo
- : unit = ()
```

Those variables are then available in the subsequent blocks

```ocaml
# print_endline (Sys.getenv "FOO")
bar
- : unit = ()
```

Sections

It is possible to test or execute only a subset of the file using sections using the --section option (short name is -s). For instance ocaml-mdx pp -s foo will only consider the section matching the perl regular expression foo.

Dependencies (13)

  1. odoc-parser >= "1.0.0" & < "2.3.0"
  2. ocaml-version >= "2.3.0"
  3. result
  4. re >= "1.7.2"
  5. cmdliner >= "1.0.0"
  6. logs >= "0.7.0"
  7. astring
  8. csexp >= "1.3.2"
  9. cppo build & >= "1.1.0"
  10. fmt >= "0.8.7"
  11. ocamlfind
  12. ocaml >= "4.08.0" & < "5.0.0"
  13. dune >= "2.7"

Dev Dependencies (4)

  1. odoc with-doc
  2. cmdliner with-test & < "1.1.0"
  3. alcotest with-test
  4. lwt with-test

Used by (85)

  1. bastet
  2. bastet_async
  3. bastet_lwt
  4. calculon = "0.4"
  5. calculon-web = "0.4"
  6. carbon
  7. castore >= "0.0.2"
  8. cconv-ppx
  9. cohttp-eio
  10. containers-data >= "3.11"
  11. current_examples >= "0.6"
  12. current_git >= "0.6.1"
  13. current_incr >= "0.6.0"
  14. datalog >= "0.6"
  15. diskuvbox
  16. dkml-c-probe
  17. dolmen >= "0.8"
  18. dolmen_loop >= "0.9"
  19. domain-local-await < "1.0.1"
  20. domain-local-timeout < "1.0.1"
  21. dune-release >= "1.4.0" & < "1.5.2"
  22. eio = "0.8.1"
  23. eio_linux < "0.9"
  24. eio_luv >= "0.6" & < "0.9"
  25. eio_main >= "0.2" & < "0.9"
  26. electrod >= "0.5"
  27. ezcurl-lwt
  28. fuseau
  29. geojson
  30. geojsone
  31. gitlab < "0.1.1"
  32. gitlab-jsoo < "0.1.1"
  33. gitlab-unix < "0.1.1"
  34. hilite
  35. http-cookie >= "4.3.0"
  36. http-date
  37. ISO3166
  38. irmin-cli
  39. irmin-unix >= "3.0.0"
  40. iter != "1.2.1"
  41. kcas >= "0.2.1" & < "0.7.0"
  42. kcas_data < "0.7.0"
  43. lab
  44. lambda_streams
  45. lwt_eio
  46. merlin >= "3.3.0" & != "3.3.4~4.10preview1" & < "3.8.0"
  47. moonpool
  48. msat = "0.8"
  49. multibase
  50. multicodec
  51. multihash-digestif
  52. odoc >= "2.0.0" & < "2.3.0"
  53. ortac-qcheck-stm
  54. owi
  55. pa_ppx_parsetree < "0.02"
  56. pa_ppx_quotation2extension < "0.02"
  57. pa_ppx_regexp < "0.02"
  58. pa_ppx_static < "0.02"
  59. pari
  60. pari-bindings
  61. polars
  62. polars_async
  63. ppx_deriving_yaml
  64. preface >= "1.0.0"
  65. pretty_expressive = "0.2"
  66. printbox-html >= "0.7"
  67. printbox-md
  68. printbox-text
  69. reparse >= "3.0.0"
  70. routes >= "1.0.0"
  71. search
  72. spelll >= "0.3"
  73. spin = "0.6.0"
  74. thread-table < "1.0.0"
  75. tls-eio
  76. toml >= "7.1.0"
  77. topojson
  78. topojsone
  79. um-abt
  80. uring >= "0.4"
  81. wtr
  82. wtr-ppx
  83. yaml >= "1.0.0"
  84. yaml-sexp
  85. zarith-ppx

Conflicts

None