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 thecrash
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)
// ^
```
reset-class
The :reset-class
modifier is like :reset
except that it wraps the following statements in a class instead of an object. By default, statements are wrapped in an object but that encoding can cause problems such as deadlocks during initialization of multi-threaded code. See this StackOverflow answer for a more detailed explanation.
For example, the following program crashes by default with a timeout exception.
Before:
```scala mdoc
import scala.concurrent._, duration._, mdoc.docs.executor
val x = 1
Await.result(Future(x), Duration("10ms"))
```
Error:
error: modifiers.md:4:1: Futures timed out after [10 milliseconds]
Await.result(Future(x), Duration("10ms"))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
java.lang.ExceptionInInitializerError
at repl.Session$.app(modifiers.md:3)
Caused by: java.util.concurrent.TimeoutException: Futures timed out after [10 milliseconds]
at scala.concurrent.impl.Promise$DefaultPromise.ready(Promise.scala:259)
at scala.concurrent.impl.Promise$DefaultPromise.result(Promise.scala:263)
at scala.concurrent.Await$.$anonfun$result$1(package.scala:220)
at scala.concurrent.BlockContext$DefaultBlockContext$.blockOn(BlockContext.scala:57)
at scala.concurrent.Await$.result(package.scala:146)
at repl.Session$App$.<init>(modifiers.md:14)
at repl.Session$App$.<clinit>(modifiers.md)
... 1 more
Use :reset-class
to avoid the timeout exception.
Before:
```scala mdoc:reset-class
import scala.concurrent._, duration._, mdoc.docs.executor
val x = 1
Await.result(Future(x), Duration("10ms"))
```
After:
```scala
import scala.concurrent._, duration._, mdoc.docs.executor
val x = 1
// x: Int = 1
Await.result(Future(x), Duration("10ms"))
// res1: Int = 1
```
Note that :reset-class
does not support the some language constructs including:
- value classes: classes that extend
AnyVal
must be toplevel or enclosed in objects - final classes: pattern matching against final inner classes causes "The outer reference in this type test cannot be checked at run time." warnings under
-Xfatal-warnings
.
to-string
The toString
modifier changes the pretty-printer for runtime values to use Object.toString
instead of PPrint.
Before:
```scala mdoc:to-string
List("no quotes")
```
```scala mdoc
List("with quotes")
```
After:
```scala
List("no quotes")
// res0: List[String] = List(no quotes)
```
```scala
List("with quotes")
// res1: List[String] = List("with quotes")
```
compile-only
The compile-only
modifier ensures the code example compiles without evaluating the program at runtime. This can be helpful to demonstrate code examples that perform side-effects.
Before:
```scala mdoc:compile-only
val name = scala.io.StdIn.readLine("Enter your name: ")
```
After:
```scala
val name = scala.io.StdIn.readLine("Enter your name: ")
```
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)
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""
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:

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