Skip to content

Commit

Permalink
Add post modifiers to support custom renderings like charts
Browse files Browse the repository at this point in the history
Post modifiers enable users to fully customize the rendering of
evaluated code blocks based on the static type and runtime values of
variables in the code fence. One example use-case for post modifiers is
rendering charts, but I'm sure we can come up with other examples as
well.
  • Loading branch information
olafurpg committed Oct 23, 2018
1 parent 83890de commit 361006e
Show file tree
Hide file tree
Showing 30 changed files with 739 additions and 152 deletions.
6 changes: 6 additions & 0 deletions build.sbt
Expand Up @@ -97,7 +97,9 @@ lazy val unit = project
IO.write(props, "mdoc properties", out)
List(out)
},
resolvers += Resolver.bintrayRepo("cibotech", "public"),
libraryDependencies ++= List(
"com.cibo" %% "evilplot" % "0.6.0",
"co.fs2" %% "fs2-core" % "0.10.4",
"org.scalacheck" %% "scalacheck" % "1.13.5" % Test,
"org.scalacheck" %% "scalacheck" % "1.13.5" % Test,
Expand All @@ -118,6 +120,10 @@ lazy val docs = project
.in(file("mdoc-docs"))
.settings(
skip in publish := true,
resolvers += Resolver.bintrayRepo("cibotech", "public"),
libraryDependencies ++= List(
"com.cibo" %% "evilplot" % "0.6.0",
),
test := run.in(Compile).toTask(" --test").value,
watchSources += baseDirectory.in(ThisBuild).value / "docs",
cancelable in Global := true,
Expand Down
Binary file added docs/evilplot.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
162 changes: 121 additions & 41 deletions docs/readme.md
Expand Up @@ -38,6 +38,8 @@ Table of contents:
- [Crash](#crash)
- [Passthrough](#passthrough)
- [Invisible](#invisible)
- [PostModifier](#postmodifier)
- [StringModifier](#stringmodifier)
- [Scastie](#scastie)
- [Key features](#key-features)
- [Performance](#performance)
Expand Down Expand Up @@ -281,6 +283,119 @@ println("I am invisible")
More prose.
````

### 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.

![](evilplot.gif)

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

```scala mdoc:file:mdoc-docs/src/main/scala/mdoc/docs/EvilplotModifier.scala

```

Next, create a resource file `META-INF/services/mdoc.PostModifier` so the post
modififer is recognized by the JVM
[ServiceLoader](https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html)
framework.

```scala mdoc:file:mdoc-docs/src/main/resources/META-INF/services/mdoc.PostModifier

```

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

````scala mdoc:mdoc
```scala mdoc:evilplot: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()
```
````

Which renders into a scatter plot like this:

![](scatterplot.png)

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.

````scala mdoc:mdoc:crash
```scala mdoc:evilplot:scatterplot.png
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.

```scala mdoc:silent
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
}
}
```

Pass the custom modifier to `MainSettings.withStringModifiers(List(...))`.

```scala
val settings = mdoc.MainSettings()
.withStringModifiers(List(
new FooModifier
))
```

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

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

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

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

### Scastie

The `scastie` modifier transforms a Scala code block into a
Expand Down Expand Up @@ -464,48 +579,13 @@ Install version @@@OLD_VERSION@

### Extensible

When using the library API, it's possible to implement custom modifiers by
extending `mdoc.StringModifier`.
The mdoc library API enables users to implement "modifiers" that customize the
rendering of mdoc code fences. There are two kinds of modifiers:

```scala
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
}
}
```

Pass the custom modifier to `MainSettings.withStringModifiers(List(...))`.

```scala
val settings = mdoc.MainSettings()
.withStringModifiers(List(
new FooModifier
))
```

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

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

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

````scala mdoc:mdoc
```scala mdoc:foo:crash
Hello world!
```
````
- [PostModifier](#postmodifier): post-process a compiled and interpreted mdoc
code fence.
- [StringModifier](#stringmodifier): process code fences as plain string values
without mdoc compilation or intepretation.

⚠️ This feature is under development and is likely to have breaking changes in
future releases.
Expand Down
Binary file added evilplot.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1 @@
mdoc.docs.EvilplotModifier
@@ -0,0 +1 @@
mdoc.docs.FileModifier
5 changes: 3 additions & 2 deletions mdoc-docs/src/main/scala/mdoc/docs/Docs.scala
Expand Up @@ -4,6 +4,7 @@ import java.nio.file.Paths
import scala.meta.internal.io.PathIO
import mdoc.Main
import mdoc.MainSettings
import mdoc.StringModifier
import mdoc.internal.BuildInfo
import mdoc.modifiers.ScastieModifier

Expand All @@ -24,7 +25,7 @@ object Docs {
.withCleanTarget(false)
.withReportRelativePaths(true)
.withStringModifiers(
List(
StringModifier.default() ++ List(
new FooModifier,
new ScastieModifier(debugClassSuffix = Some("<a_random_uuid>"))
)
Expand All @@ -34,7 +35,7 @@ object Docs {
val exitCode = Main.process(
settings
.withStringModifiers(
List(
StringModifier.default() ++ List(
new FooModifier,
new MdocModifier(context)
)
Expand Down
38 changes: 38 additions & 0 deletions mdoc-docs/src/main/scala/mdoc/docs/EvilplotModifier.scala
@@ -0,0 +1,38 @@
package mdoc.docs

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

class EvilplotModifier extends PostModifier {
val name = "evilplot"
def process(ctx: PostModifierContext): String = {
val out = ctx.outputFile.resolveSibling(_ => ctx.info)
ctx.lastValue match {
case d: Drawable =>
Files.createDirectories(out.toNIO.getParent)
if (out.isFile) {
Files.delete(out.toNIO)
}
d.write(out.toFile)
s"![](${out.toNIO.getFileName})"
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"""
)
""
}
}
}
35 changes: 35 additions & 0 deletions mdoc-docs/src/main/scala/mdoc/docs/FileModifier.scala
@@ -0,0 +1,35 @@
package mdoc.docs

import java.nio.charset.StandardCharsets
import mdoc.Reporter
import mdoc.StringModifier
import scala.meta.inputs.Input
import scala.meta.inputs.Position
import scala.meta.internal.io.FileIO
import scala.meta.io.AbsolutePath
import mdoc.internal.pos.PositionSyntax._

class FileModifier extends StringModifier {
val name = "file"
override def process(
info: String,
code: Input,
reporter: Reporter
): String = {
val file = AbsolutePath(info)
if (file.isFile) {
val text = FileIO.slurp(file, StandardCharsets.UTF_8)
s"""
File: [${file.toNIO.getFileName}](https://github.com/olafurpg/mdoc/blob/master/$info)
`````scala
$text
`````
"""
} else {
val pos = Position.Range(code, 0 - info.length - 1, 0 - 1).toUnslicedPosition
reporter.error(pos, s"no such file: $file")
""
}
}

}

0 comments on commit 361006e

Please sign in to comment.