Suited is cool and I’ve given many talks using it and the same document can be a blog or article too.
But we always want it to do more for instance we wanted to write out talks in markdown.
So we needed a markdown plugin, but we also wanted a magic syntax to add slides and figures in markdown,
Suited.js uses <section data-figure> and <section data-slide> to mark out sections of the contents to appear as navigable
sections. Slides are just visible in the slide show whereas figures are visible in doc mode and also in the slide show.
markdown slides
~~* **your bold stuff** *~~
markdown figures
~~: **your bold and _italic_ stuff** :~~
So we implemented a markdown plugin, but delegated to markdown-it.
But suited has a fatal flaw. It has no parser to implement fragments
suited.js
Fragments are no fun
fragments
1234567891011121314151617
<section data-slide>
### Why?
*
-
</section>
<section data-slide>
### Why?
* because
-
</section>
<section data-figure>
### Why?
* because
- it's like that!
</section>
markdown-it plugin?
I dont want to write another markdownit plugin
especially a complex one
it will probably add javascript to the markup
so I wrote my own markdown parser for Javascript?
Using Scala.js
how hard can it be?
Because I want to rely on a few libraries as possible while experimenting with Scala.js I decided to use
Mark Hibberd’s Combinator Parser demo code from his previous
Scalasyd talk “Dont fear the parser” as a starting point.
// syntax pimpsimplicitclassTransformerOps[T0](foo:T0){/** * magic wand. pimp alias of Transformer.run eg a transform function */def---*[A,B](bar:A)(implicitaux:Transformer.Aux[T0,A,B]):B={aux.run(foo,bar)}...}
Now lets look at a specific member of the Transformer typeclass. The MarkdownToHtml transformer.
I wanted to split the transform function into two parts, one to parse, and of course I want to use my markdown parser’s run function in this place, and one to render the ParseResult into HTML.
Not only does this make it easier to reason about but also (I Hope) easier to build a plugin interface where you can supply a function to parse modified markdown as long as it still produces a ParseResult[Markdown] and one to enrich the HTML or even transform it completely, say into rich text or PDF
objectMarkdownToHtml{importTransformer._importast._// Use `instance` typeclass constructor to add MarkdownToHtml to the Transformer typeclassimplicitvalm2hTransformer:Transformer.Aux[MarkdownToHtml,String, parser.Html]=instance((t,in)=>t.r(t.p(in)))...}
Notice that this is a successful parse, and that the ParseOK case class also contains the remaining input for further processing
Also notice that the H2 contains a List of more Markdown, in this case rawHtml.
This is because the link text could contain more inline markdown, like so…
Demo headerParser
1
scala>headerParser.run("""## this _is_ h2 i **presume**""")
scala>headerParser.run("""this _is_ NOT h2 i presume""")
1234
scala>headerParser.run("""this _is_ NOT h2 i presume""")headerParser.run("""this _is_ NOT h2 i presume""")res0:parser.ParseState[ast.Markdown]=ParseKo(Inputfailedtomatchpredicate.:insteadsawchar't')
Admittedly, this is not the best error message (for better errors use the built in Scala combinator parser library rather than this demo).
It failed to see a # as the first char in a header and so produced a ParseKo instead of ParseOk.
The important thing to see is that a parser stops as soon as it can go no further.
In order to parse a whole document we can use the combinator methods, such as ||| i.e. or on Parser and some of the parsers such a list to compose more powerful
parsers that continue parsing input by trying one parser after another.
An example is the markdownParser which will try all blockParsers followed by the inlineParsers continuously until it can parse no more.
However that will only occur when there is no more input because the last parser it tries is always the rawHtml parser which will always succeed because any input that is not parsed as markdown must be is just treated as RawHtml.
Demo markdownParser
12
scala>markdownParser.run("""this _is_ NOT h2 i presume | ## but this _is **h2**_""")
Now we can see that the input parses into a List of Markdown. This is all very well but it’d be nice to see the rendered HTML.
This is where simple MarkdownToHtml Transformer comes in.
I can now use the ---* magic wand function
Demo simple render
12
scala>simple---*"""this _is_ NOT h2 i presume | ## but this _is **h2**_"""
1234
res1:transformers.MarkdownToHtml.m2hTransformer.OUT="this <em>is</em> NOT h2 i presume<h2>but this <em>is <strong>h2</strong></em></h2>"
Demo simple render a complex list
12345678
scala>simple---*"""* unordered list item | 1. _nested_ list item | 2. **another _nested_** list item | * unorderd [link](http://foo.bar) list item | | [a ref link][1] whose url detail is at the end | | [1] http://the.reflink.com"""
Demo simple render a complex list result
1234567891011121314
res0:transformers.MarkdownToHtml.m2hTransformer.OUT="<ul> <li>unordered list item</li> <ol> <li><em>nested</em> list item</li> <li><strong>another <em>nested</em></strong> list item</li> </ol> <li>unorderd <a href="http://foo.bar">link</a> list item</li></ul><a href="http://the.reflink.com">a ref link</a> whose url detail is at the end<p><!-- [1] http://the.reflink.com --></p>"
So… Performance?
What about Performance?
I haven’t run proper benchmarks yet, but as a quick and dirty test
I have added timing output to the main function.
remember this in build.sbt
1
scalaJSUseMainModuleInitializer:=true
After compiling to JavaScript
adds a call to the main function in the MainClass
at the end of the JavaScript.
This makes the main function run when the JS is loaded
The TestApp just renders 112 lines of markdown into HTML a few times.
We can see that after the initial run where it starts the node environment it ends up taking about 430ms.
That seems slow!
seems a bit slow!
112 lines of markdown in 430ms
seems a bit slow!
112 lines of markdown in 430ms
lets compare the JS to a scala/JVM run
If we comment the JS lines in build.sbt we can build and run the main app in standard Scala on the JVM
comment Scala.js from build.sbt
123456789
// enablePlugins(ScalaJSPlugin)// This is an application with a main method// change this to true if you want the The TestApp main class to be a JS "Application"// scalaJSUseMainModuleInitializer := truename:="Markdownem"scalaVersion:="2.12.2"...
But this is not really a fair comparison with NodeJS
On the JVM same code runs 200ms faster
112 lines of markdown in ~ 230ms
But this is not really a fair comparison with NodeJS
The Javascript was not fully optomised because it would take longer to compile
On the JVM same code runs 200ms faster
112 lines of markdown in ~ 230ms
But this is not really a fair comparison with NodeJS
The Javascript was not fully optomised because it would take longer to compile
So lets optomise it.
use the optimising compiler
The Javascript was compiled with the FastOptStage compiler
meaning it was fast to compile
We can tell sbt to run with the FullOptStage compiler
which will optimise much more
un-comment Scala.js, put it back into build.sbt
123456789
enablePlugins(ScalaJSPlugin)// This is an application with a main method// change this to true if you want the The TestApp main class to be a JS "Application"scalaJSUseMainModuleInitializer:=truename:="Markdownem"scalaVersion:="2.12.2"...