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 errors but program compiled successfully without errors
val x: String = ""
^^^^^^^^^^^^^^^^^^
Note that
fail
does not assert that the program compiles but crashes at runtime. To assert runtime exceptions, use thecrash
modifier.
warn
The warn
modifier is similar to fail
except that it asserts the code
compiles successfully but with a warning message.
Before:
```scala mdoc:warn
List(1) match {
case Nil =>
}
```
After:
```scala
List(1) match {
case Nil =>
}
// warning: match may not be exhaustive.
// It would fail on the following input: List(_)
// List(1) match {
// ^^^^^^^
```
The build fails when a warn
code fence compiles without warnings.
Before:
```scala mdoc:warn
List(1) match {
case head :: tail =>
case Nil =>
}
```
Error:
error: modifiers.md:2:1: Expected compile warnings but program compiled successfully without warnings
List(1) match {
^
The build also fails when a warn
code fence fails to compile, even if the
program has a warning. Use fail
in these cases instead.
Before:
```scala mdoc:warn
val x: Int = ""
```
Error:
error: modifiers.md:2:1: Expected compile warnings but program failed to compile
val x: Int = ""
^^^^^^^^^^^^^^^
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.MdocSession$MdocApp$$anonfun$1.apply$mcV$sp(modifiers.md:9)
// at repl.MdocSession$MdocApp$$anonfun$1.apply(modifiers.md:8)
// at repl.MdocSession$MdocApp$$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
This modifier works exactly the same way as :reset
and is deprecated. Please
use :reset
instead.
reset-object
The :reset-object
modifier is similar to :reset
except that it wraps the
following statements in a object instead of a class. By default, statements are
wrapped in an class but that encoding can cause problems compile errors such as
- "Value class may not be a member of another class": happens when using
extends AnyVal
- "The outer reference in this type test cannot be checked at run time": happens when pattern matching on sealed ADT.
Example case where the default class wrapping does not compile:
Before:
```scala mdoc
final case class Name(val value: String) extends AnyVal
```
Error:
error: modifiers.md:2:1: value class may not be a member of another class
final case class Name(val value: String) extends AnyVal
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Use :reset-object
to avoid these compile errors
Before:
```scala mdoc:reset-object
final case class Name(val value: String) extends AnyVal
```
After:
```scala
final case class Name(val value: String) extends AnyVal
```
nest
The nest
modifier starts a new scope similar to reset
but previous variables
in the scope are still accessible. This can be helpful to redefine variable
names while still being able to access existing variables in scope.
Before:
```scala mdoc
val x = 40
val y = 1
```
```scala mdoc:nest
val x: Int = 41
println(x + y)
```
After:
```scala
val x = 40
// x: Int = 40
val y = 1
// y: Int = 1
```
```scala
val x: Int = 41
// x: Int = 41
println(x + y)
// 42
```
Without nest
in the example above, the document would fail to compile because
the variable x
was already defined in scope.
A nest
block wraps subsequent code blocks inside
_root_.scala.Predef.locally{...}
. This encoding means that it's not possible
to define value classes inside a nested block.
Before:
```scala mdoc
val x: Int = 41
```
```scala mdoc:nest
case class Foo(val y: Int) extends AnyVal
Foo(x)
```
Error:
error: modifiers.md:6:1: value class may not be a local class
case class Foo(val y: Int) extends AnyVal
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Also, nested scopes do not ambiguate between conflicting implicits.
Before:
```scala mdoc
implicit val x: Int = 41
```
```scala mdoc:nest
implicit val y: Int = 41
implicitly[Int]
```
Error:
error: modifiers.md:7:1: ambiguous implicit values:
both value x in class MdocApp of type => Int
and value y of type Int
match expected type Int
implicitly[Int]
^^^^^^^^^^^^^^^
A workaround for ambiguous implicits in nested scopes is to shadow one implicit by redefining its variable name.
Before:
```scala mdoc
implicit val shadowMe: Int = 41
```
```scala mdoc:nest
implicit val shadowMe: Int = 42
implicitly[Int]
```
After:
```scala
implicit val shadowMe: Int = 41
// shadowMe: Int = 41
```
```scala
implicit val shadowMe: Int = 42
// shadowMe: Int = 42
implicitly[Int]
// res0: Int = 42
```
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")
```
width
The width=NUMBER
allows you to override max width (deault is 80) of
pretty-printed values.
Before:
```scala mdoc:width=20
List.fill(2)(List(1,2,3,4,5))
```
```scala mdoc
List.fill(2)(List(1,2,3,4,5))
```
After:
```scala
List.fill(2)(List(1,2,3,4,5))
// res0: List[List[Int]] = List(
// List(
// 1,
// 2,
// 3,
// 4,
// 5
// ),
// List(
// 1,
// 2,
// 3,
// 4,
// 5
// )
// )
```
```scala
List.fill(2)(List(1,2,3,4,5))
// res1: List[List[Int]] = List(List(1, 2, 3, 4, 5), List(1, 2, 3, 4, 5))
```
height=
The height=NUMBER
allows you to override max height (default is 50) of
pretty-printed values.
Before:
```scala mdoc:height=5
List.fill(15)("hello world!")
```
```scala mdoc
List.fill(15)("hello world!")
```
After:
```scala
List.fill(15)("hello world!")
// res0: List[String] = List(
// "hello world!",
// "hello world!",
// "hello world!",
// ...
```
```scala
List.fill(15)("hello world!")
// res1: List[String] = List(
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!",
// "hello world!"
// )
```
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-b9dc0228-56bf-4874-8195-ce068384baed'></pre>
<script>window.addEventListener('load', function() {
scastie.Embedded('.scastie-snippet-b9dc0228-56bf-4874-8195-ce068384baed', {
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"![](${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