mdoc

mdoc

  • Docs
  • Blog
  • GitHub

›Modifiers

Overview

  • Installation
  • Why mdoc?
  • Coming from tut

Modifiers

  • JVM
  • Scala.js

Site generators

  • Docusaurus
Edit

JVM Modifiers

Code fences with the scala mdoc modifier are compiled and evaluated on the JVM at markdown generation time.

Default

The default modifier compiles and executes the code fence as normal

Before:

```scala mdoc
val x = 1
val y = 2
x + y
```

After:

```scala
val x = 1
// x: Int = 1
val y = 2
// y: Int = 2
x + y
// res0: Int = 3
```

Silent

The silent modifier is identical to the default modifier except that it hides the evaluated output. The input code fence renders unchanged.

Before:

```scala mdoc:silent
val x = 1
val y = 2
x + y
```
```scala mdoc
x + y
```

After:

```scala
val x = 1
val y = 2
x + y
```

```scala
x + y
// res1: Int = 3
```

Fail

The fail modifier asserts that the code block will not compile. The rendered output contains the type error message.

Before:

```scala mdoc:fail
val x: Int = ""
```

After:

```scala
val x: Int = ""
// error: type mismatch;
//  found   : String("")
//  required: Int
// val x: Int = ""
//              ^^
```

A fail code fence with no compile error fails the build.

Before:

```scala mdoc:fail
val x: String = ""
```

Error:

error: modifiers.md:2:1: Expected compile error but statement typechecked successfully
val x: String = ""
^^^^^^^^^^^^^^^^^^

Note that fail does not assert that the program compiles but crashes at runtime. To assert runtime exceptions, use the crash modifier.

Crash

The crash modifier asserts that the code block throws an exception at runtime

Before:

```scala mdoc:crash
val y = ???
```

After:

```scala
val y = ???
// scala.NotImplementedError: an implementation is missing
//  at scala.Predef$.$qmark$qmark$qmark(Predef.scala:288)
//  at repl.Session$App$$anonfun$1.apply$mcV$sp(modifiers.md:9)
//  at repl.Session$App$$anonfun$1.apply(modifiers.md:8)
//  at repl.Session$App$$anonfun$1.apply(modifiers.md:8)
```

Passthrough

The passthrough modifier collects the stdout and stderr output from the program and embeds it verbatim in the markdown file.

Before:

```scala mdoc:passthrough
val matrix = Array.tabulate(4, 4) { (a, b) =>
  val multiplied = (a + 1) * (b + 1)
  f"$multiplied%2s"
}
val table = matrix.map(_.mkString("| ", " | ", " |")).mkString("\n")
println(s"""
This will be rendered as markdown.

* Bullet 1
* Bullet 2

Look at the table:

$table
""")
```

After:

This will be rendered as markdown.

* Bullet 1
* Bullet 2

Look at the table:

|  1 |  2 |  3 |  4 |
|  2 |  4 |  6 |  8 |
|  3 |  6 |  9 | 12 |
|  4 |  8 | 12 | 16 |

Invisible

The invisible modifier evaluates the code but does not render anything. The invisible modifier is equivalent to passthrough when the expression does not print to stdout.

Before:

This is prose.
```scala mdoc:invisible
println("I am invisible")
```
More prose.

After:

This is prose.

More prose.

Reset

The reset modifier starts a new scope where previous statements in the document are no longer available. This can be helpful to clear existing imports or implicits in scope.

Before:

```scala mdoc
implicit val x: Int = 41
```

```scala mdoc:reset
implicit val y: Int = 42
implicitly[Int] // x is no longer in scope
```
```scala mdoc:fail
println(x)
```

After:

```scala
implicit val x: Int = 41
// x: Int = 41
```

```scala
implicit val y: Int = 42
// y: Int = 42
implicitly[Int] // x is no longer in scope
// res1: Int = 42
```

```scala
println(x)
// error: not found: value x
// println(x)
//         ^
```

PostModifier

A PostModifier is a custom modifier that post-processes a compiled and interpreted mdoc code fence. Post modifiers 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.

One example use-case for post modifiers is to render charts based on the runtime value of the last expression in the code fence.

Extend the mdoc.PostModifier trait to implement a post modifier.

File: EvilplotModifier.scala

package mdoc.docs

import com.cibo.evilplot.geometry.Drawable
import java.nio.file.Files
import java.nio.file.Paths
import mdoc._
import scala.meta.inputs.Position

class EvilplotModifier extends PostModifier {
  val name = "evilplot"
  def process(ctx: PostModifierContext): String = {
    val relpath = Paths.get(ctx.info)
    val out = ctx.outputFile.toNIO.getParent.resolve(relpath)
    ctx.lastValue match {
      case d: Drawable =>
        Files.createDirectories(out.getParent)
        if (!Files.isRegularFile(out)) {
          d.write(out.toFile)
        }
        s"![](${ctx.info})"
      case _ =>
        val (pos, obtained) = ctx.variables.lastOption match {
          case Some(variable) =>
            val prettyObtained =
              s"${variable.staticType} = ${variable.runtimeValue}"
            (variable.pos, prettyObtained)
          case None =>
            (Position.Range(ctx.originalCode, 0, 0), "nothing")
        }
        ctx.reporter.error(
          pos,
          s"""type mismatch:
  expected: com.cibo.evilplot.geometry.Drawable
  obtained: $obtained"""
        )
        ""
    }
  }
}

Next, create a resource file META-INF/services/mdoc.PostModifier so the post modififer is recognized by the JVM ServiceLoader framework.

File: mdoc.PostModifier

mdoc.docs.EvilplotModifier

As long as EvilplotModifier is available on the classpath, for example via libraryDependencies in build.sbt, then you can use the modifier like this.

Before:

```scala mdoc:evilplot:assets/scatterplot.png
import com.cibo.evilplot._
import com.cibo.evilplot.plot._
import com.cibo.evilplot.plot.aesthetics.DefaultTheme._
import com.cibo.evilplot.numeric.Point

val data = Seq.tabulate(90) { i =>
  val degree = i * 8
  val radian = math.toRadians(degree)
  Point(i.toDouble, math.sin(radian))
}

ScatterPlot(data)
  .xAxis()
  .yAxis()
  .frame()
  .xLabel("x")
  .yLabel("y")
  .render()
```

After:

![](assets/scatterplot.png)

Which renders into a scatter plot like this:

It's important that post modifiers present helpful error messages to the user in case of failures. For example, if the last runtime value is not an EvilPlot Drawable we can report the expected and obtained types with carets pointing to the position of the last variable.

Before:

```scala mdoc:evilplot:scatterplot.png
val message = "hello world!"
```

Error:

error: modifiers.md:2:5: type mismatch:
  expected: com.cibo.evilplot.geometry.Drawable
  obtained: String = hello world!
val message = "hello world!"
    ^^^^^^^

StringModifier

A StringModifier is a custom modifier that processes the plain text contents of a code block, ignoring the compilation and interpretation of the Scala code.

import mdoc.StringModifier
import mdoc.Reporter
import scala.meta.Input
class FooModifier extends StringModifier {
  override val name = "foo"
  override def process(info: String, code: Input, reporter: Reporter): String = {
    val originalCodeFenceText = code.text
    val isCrash = info == "crash"
    if (isCrash) "BOOM"
    else "OK: " + originalCodeFenceText
  }
}

Next, create a resource file META-INF/services/mdoc.StringModifier so the post modififer is recognized by the JVM ServiceLoader framework.

File: mdoc.StringModifier

mdoc.docs.FooModifier
mdoc.docs.SbtModifier
mdoc.docs.FileModifier

Code blocks with the mdoc:foo modifier will render as follows.

Before:

```scala mdoc:foo
Hello world!
```

After:

OK: Hello world!

We can also add the argument :crash to render "BOOM".

Before:

```scala mdoc:foo:crash
Hello world!
```

After:

BOOM

Scastie

The scastie modifier transforms a Scala code block into a Scastie snippet.

ℹ️ This modifier will work only in environments that support embedding a <script> tag. For example, it won't work in GitHub readmes, but it will work when building a static website from Markdown (e.g., with Docusaurus)

You can embed an existing Scastie snippet by its id:

Before:

```scala mdoc:scastie:xbrvky6fTjysG32zK6kzRQ

```

After:

<script src='https://scastie.scala-lang.org/xbrvky6fTjysG32zK6kzRQ.js?theme=light'></script>

or in case of a user's snippet:

Before:

```scala mdoc:scastie:MasseGuillaume/CpO2s8v2Q1qGdO3vROYjfg

```

After:

<script src='https://scastie.scala-lang.org/MasseGuillaume/CpO2s8v2Q1qGdO3vROYjfg.js?theme=light'></script>

⚠️ The empty line in the block can't be omitted due to how the Markdown parser works

Moreover, you can quickly translate any Scala code block block into a Scastie snippet on the fly.

Before:

```scala mdoc:scastie
val x = 1 + 2
println(x)
```

After:

<script src="https://scastie.scala-lang.org/embedded.js"></script>

<pre class='scastie-snippet-<a_random_uuid>'></pre>

<script>window.addEventListener('load', function() {
scastie.Embedded('.scastie-snippet-<a_random_uuid>', {
code: `val x = 1 + 2
println(x)`,
theme: 'light',
isWorksheetMode: true,
targetType: 'jvm',
scalaVersion: '2.12.6'
})
})</script>

⚠️ Inline snippets are slower to run than embedded ones, since they won't be cached. You should prefer embedding existing snippets whenever possible.

You can choose the Scastie theme when initializing the Scastie modifier:

import mdoc.modifiers.ScastieModifier
new ScastieModifier(theme = "dark") // default is "light"
// res0: ScastieModifier = StringModifier(mdoc:scastie)
← Coming from tutScala.js →
  • Default
  • Silent
  • Fail
  • Crash
  • Passthrough
  • Invisible
  • Reset
  • PostModifier
  • StringModifier
  • Scastie
mdoc
Docs
Get started
Community
Chat on Gitter
More
GitHub
Copyright © 2019 mdoc developers