mdoc

mdoc

  • Docs
  • Blog
  • GitHub

›Overview

Overview

  • Installation
  • Why mdoc?
  • Coming from tut

Modifiers

  • JVM
  • Scala.js

Site generators

  • Docusaurus
Edit

Why mdoc?

The distinguishing features of mdoc are the following.

  • good performance: markdown documents compile as a single Scala program and evaluate in one run. Combined with incremental and hot compilation from file watching, medium sized documents can generate in a few hundred milliseconds.
  • program semantics: markdown documents are compiled into normal Scala programs making language features work as you expect them to.
  • good error messages: compile errors and crashes are reported with positions of the original markdown source making it easy to track down where things went wrong.
  • link hygiene: catch broken links to non-existing sections while generating the site.
  • variable injection: use variables like @VERSION@ to that make sure the documentation stays up-to-date with new releases.
  • extensibility: custom modifiers allow you to programmatically control how the resulting markdown gets rendered.

Performance

mdoc is designed to provide a tight edit/render/preview feedback loop while writing documentation. mdoc achieves good performance through

  • program semantics: each markdown file compiles into a single Scala program that executes in one run.
  • being incremental: with --watch, mdoc compiles individual files as they change avoiding unnecessary work re-generating the full site.
  • keeping the compiler hot: with --watch, mdoc re-uses the same Scala compiler instance for subsequent runs making compilation faster after a few iterations. A medium sized document can go from compiling in ~5 seconds with a cold compiler down to 500ms with a hot compiler.

Good error messages

mdoc tries to report helpful error messages when things go wrong. Here below, the program that is supposed to compile successfully but it has a type error so the build is stopped with an error message from the Scala compiler.

Before:

```scala mdoc
val typeError: Int = "should be int"
```

Error:

error: why.md:2:22: type mismatch;
 found   : String("should be int")
 required: Int
val typeError: Int = "should be int"
                     ^^^^^^^^^^^^^^^

Here below, the programs are supposed to fail due to the fail and crash modifiers but they succeed so the build is stopped with an error message from mdoc.

Before:

```scala mdoc:fail
val noFail = "success"
```
```scala mdoc:crash
val noCrash = "success"
```

Error:

error: why.md:2:1: Expected compile error but statement typechecked successfully
val noFail = "success"
^^^^^^^^^^^^^^^^^^^^^^
error: why.md:5:1: Expected runtime exception but program completed successfully
val noCrash = "success"
^^^^^^^^^^^^^^^^^^^^^^^

Observe that positions of the reported diagnostics point to line numbers and columns in the original markdown document. Internally, mdoc instruments code fences to extract metadata like variable types and runtime values. Positions of error messages in the instrumented code are translated into positions in the markdown document.

Link hygiene

Docs get quickly out date, in particular links to different sections. After generating a site, mdoc analyzes links for references to non-existent sections. For the example below, mdoc reports a warning that the doesnotexist link is invalid.

Before:

# My title

Link to [my title](#my-title).
Link to [typo section](#mytitle).
Link to [old section](#doesnotexist).

Error:

warning: why.md:4:9: Unknown link 'readme.md#mytitle', did you mean 'readme.md#my-title'?
Link to [typo section](#mytitle).
        ^^^^^^^^^^^^^^^^^^^^^^^^
warning: why.md:5:9: Unknown link 'readme.md#doesnotexist'.
Link to [old section](#doesnotexist).
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Observe that mdoc suggests a fix if there exists a header that is similar to the unknown link.

Program semantics

mdoc interprets code fences as normal Scala programs instead of using the REPL. This behavior is different from tut that interprets statements as if they were typed in a REPL session. Using "program semantics" instead of "repl semantics" has benefits and downsides.

Downside: It's not possible to bind the same variable twice, for example the code below fails compilation with mdoc but compiles successfully with tut

```scala mdoc
val x = 1
val x = 1
```

Upside: Code examples from the documentation can be copy-pasted into normal Scala programs and compile.

Upside: Companion objects work as expected.

Before:

```scala mdoc
case class User(name: String)
object User {
  implicit val ordering: Ordering[User] = Ordering.by(_.name)
}
List(User("Susan"), User("John")).sorted
```

After:

```scala
case class User(name: String)
object User {
  implicit val ordering: Ordering[User] = Ordering.by(_.name)
}
List(User("Susan"), User("John")).sorted
// res0: List[User] = List(User("John"), User("Susan"))
```

Upside: Overloaded methods work as expected.

Before:

```scala mdoc
def add(a: Int, b: Int): Int = a + b
def add(a: Int): Int = add(a, 1)
add(3)
```

After:

```scala
def add(a: Int, b: Int): Int = a + b
def add(a: Int): Int = add(a, 1)
add(3)
// res0: Int = 4
```

Upside: Mutually recursive methods work as expected.

Before:

```scala mdoc
def isEven(n: Int): Boolean = n == 0 || !isOdd(n - 1)
def isOdd(n: Int): Boolean  = n == 1 || !isEven(n - 1)
isEven(8)
```

After:

```scala
def isEven(n: Int): Boolean = n == 0 || !isOdd(n - 1)
def isOdd(n: Int): Boolean  = n == 1 || !isEven(n - 1)
isEven(8)
// res0: Boolean = false
```

Upside: Compiler options like -Ywarn-unused don't report spurious errors like they do in the REPL.

$ scala -Ywarn-unused
scala> import scala.concurrent.Future
<console>:11: warning: Unused import
       import scala.concurrent.Future
                               ^
scala> Future.successful(1)
res0: scala.concurrent.Future[Int] = Future(Success(1))

Variable injection

mdoc renders variables like @VERSION@ into 1.2.3. This makes it easy to keep documentation up-to-date as new releases are published. Variables can be passed from the command-line interface with the syntax --site.VARIABLE=value.

mdoc --site.VERSION 1.0.0 --site.SCALA_VERSION 2.12.8

When using the library API, variables are passed with the MainSettings.withSiteVariables(Map[String, String]) method

 val settings = mdoc.MainSettings()
+  .withSiteVariables(Map(
+    "VERSION" -> "1.0.0",
+    "SCALA_VERSION" -> "2.12.8"
+  ))

When using sbt-mdoc, variables are defined with the mdocVariables setting.

// build.sbt
lazy val docs = project
  .settings(
+   mdocVariables := Map(
+     "VERSION" -> "1.0.0"
+   )
  )
  .enablePlugins(MdocPlugin)

Variables are replaced with the regular expression @(\w+)@. An error is reported when a variable that matches this pattern is not provided.

Before:

Install version @DOES_NOT_EXIST@

Error:

error: why.md:1:17: key not found: DOES_NOT_EXIST
Install version @DOES_NOT_EXIST@
                ^^^^^^^^^^^^^^^^

Use double @@ to escape variable injection.

Before:

Install version @@OLD_VERSION@

After:

Install version @OLD_VERSION@

Extensibility

The mdoc library APi enables you to implement custom modifiers like PostModifier, which have access to the original code fence text, the static types and runtime values of the evaluated Scala code, the input and output file paths and other contextual information.

Example usages of custom modifiers:

  • This website has a mdoc:mdoc modifier to render before/after examples of rendered markdown.
  • The Scalafmt website has a mdoc:scalafmt modifier to show before/after code examples with a given configuration.
← InstallationComing from tut →
  • Performance
  • Good error messages
  • Link hygiene
  • Program semantics
  • Variable injection
  • Extensibility
mdoc
Docs
Get started
Community
Chat on Gitter
More
GitHub
Copyright © 2019 mdoc developers