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)
// ^
```
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)