:: Article Deprecated!
:: Fantom 1.0.68 now ships with it's own MarkdownDocWriter class.
:: Article Deprecated!
Markdown is a way to read and write plain text with intuitive markup, much as you would write someone an email.
Fandoc is the plain text syntax used by Fantom for its documention, its forum and other tools. It is very similar to Markdown.
So wouldn't it be great if you could convert from one to the other!? This article shows you how...
Markdown and other plain text markup formats are great. Many others have written about their virtues, so I'll not dwell on the topic nor duplicate their content here.
Because I use Fantom, I use Fandoc in my own projects... a lot. Take the Fantom-Factory website for instance, all the articles here are written in Fandoc notation! But sometimes you just need Markdown...
For example, documentation for projects hosted on Bitbucket may be wirtten in Markdown. Taking the Slim library as an example, I don't want to maintain 2 copies of the project documentation - one for the Pod's User Guide and another for the Bitbucket home page.
So instead I elected to write the documentation in Fandoc and generate the Markdown.
Based on the upcoming FandocDocWriter, I wrote a MarkdownDocWriter class that extends the usual fandoc DocWriter. It may be used like this:
Void printMarkdown() {
fandoc := "Fandoc or Markdown
******************
Which do you prefer?"
doc := FandocParser().parseStr(fandoc)
buf := StrBuf()
wtr := MarkdownDocWriter(buf.out)
doc.write(wtr)
markdown := buf.toStr
echo(markdown)
}
Which generates the following markdown:
## Fandoc or Markdown Which do you prefer?
The MarkdownDocWriter class itself looks like:
using fandoc
class MarkdownDocWriter : DocWriter {
private ListIndex[] listIndexes := [,]
private Bool inBlockquote
private Bool inPre
private Bool inListItem
private OutStream out
|Link link|? onLink := null
new make(OutStream out) {
this.out = out
}
override Void docStart(Doc doc) {
if (doc.meta.isEmpty) return
out.printLine(Str.defVal.padl(72, '*'))
doc.meta.each |v, k| {
out.printLine("** ${k}: ${v}")
}
out.printLine(Str.defVal.padl(72, '*'))
out.printLine
}
override Void docEnd(Doc doc) { }
override Void elemStart(DocElem elem) {
switch (elem.id) {
case DocNodeId.emphasis:
out.writeChar('*')
case DocNodeId.strong:
out.print("**")
case DocNodeId.code:
out.writeChar('`')
case DocNodeId.link:
link := (Link) elem
onLink?.call(link)
out.writeChar('[')
case DocNodeId.image:
img := (Image) elem
out.print("![${img.alt}")
case DocNodeId.para:
para := (Para) elem
if (!listIndexes.isEmpty) {
indent := listIndexes.size * 2
out.printLine
out.printLine
out.print(Str.defVal.padl(indent))
}
if (inBlockquote)
out.print("> ")
if (para.admonition != null)
out.print("${para.admonition}: ")
if (para.anchorId != null)
out.print("[#${para.anchorId}]")
case DocNodeId.heading:
head := (Heading) elem
out.print(Str.defVal.padl(head.level, '#')).print(" ")
case DocNodeId.pre:
inPre = true
case DocNodeId.blockQuote:
inBlockquote = true
case DocNodeId.unorderedList:
listIndexes.push(ListIndex())
if (listIndexes.size > 1)
out.printLine
case DocNodeId.orderedList:
ol := (OrderedList) elem
listIndexes.push(ListIndex(ol.style))
if (listIndexes.size > 1)
out.printLine
case DocNodeId.listItem:
indent := (listIndexes.size - 1) * 2
out.print(Str.defVal.padl(indent))
out.print(listIndexes.peek)
listIndexes.peek.increment
inListItem = true
}
}
override Void elemEnd(DocElem elem) {
switch (elem.id) {
case DocNodeId.emphasis:
out.writeChar('*')
case DocNodeId.strong:
out.print("**")
case DocNodeId.code:
out.writeChar('`')
case DocNodeId.link:
link := (Link) elem
out.print("](${link.uri})")
case DocNodeId.image:
img := (Image) elem
out.print("](${img.uri})")
case DocNodeId.para:
out.printLine
out.printLine
case DocNodeId.heading:
head := (Heading) elem
out.printLine.printLine
case DocNodeId.pre:
inPre = false
case DocNodeId.blockQuote:
inBlockquote = false
case DocNodeId.unorderedList:
listIndexes.pop
if (listIndexes.isEmpty)
out.printLine
case DocNodeId.orderedList:
listIndexes.pop
if (listIndexes.isEmpty)
out.printLine
case DocNodeId.listItem:
item := (ListItem) elem
out.printLine
inListItem = false
}
}
override Void text(DocText text) {
if (inPre) {
endsWithLineBreak := text.str.endsWith("\n")
if (!listIndexes.isEmpty || !endsWithLineBreak) {
if (!listIndexes.isEmpty) {
out.printLine
out.printLine
}
indentNo := (listIndexes.size + 1) * 4
indent := Str.defVal.padl(indentNo)
text.str.splitLines.each {
out.print(indent).printLine(it)
}
} else {
out.printLine("```")
out.print(text.str)
out.printLine("```")
}
out.printLine
} else
out.print(text.str)
}
}
internal class ListIndex {
private static const Int:Str romans := sortr([1000:"M", 900:"CM", 500:"D", 400:"CD", 100:"C", 90:"XC", 50:"L", 40:"XL", 10:"X", 9:"IX", 5:"V", 4:"IV", 1:"I"])
OrderedListStyle? style
Int index := 1
new make(OrderedListStyle? style := null) {
this.style = style
}
This increment() {
index++
return this
}
override Str toStr() {
switch (style) {
case null:
return "- "
case OrderedListStyle.number:
return "${index}. "
case OrderedListStyle.lowerAlpha:
return "${toB26(index).lower}. "
case OrderedListStyle.upperAlpha:
return "${toB26(index).upper}. "
case OrderedListStyle.lowerRoman:
return "${toRoman(index).lower}. "
case OrderedListStyle.upperRoman:
return "${toRoman(index).upper}. "
}
throw Err("Unsupported List Style: $style")
}
private static Str toB26(Int int) {
int--
dig := ('A' + (int % 26)).toChar
return (int >= 26) ? toB26(int / 26) + dig : dig
}
private static Str toRoman(Int int) {
l := romans.keys.find { it <= int }
return (int > l) ? romans[l] + toRoman(int - l) : romans[l]
}
private static Int:Str sortr(Int:Str unordered) {
// no ordered literal map... grr...
// http://fantom.org/sidewalk/topic/1837#c14431
sorted := [:] { it.ordered = true }
unordered.keys.sortr.each { sorted[it] = unordered[it] }
return sorted
}
}
Have fun!
Edits:
- 31 Jul 2016 - Deprecated.
- 6 Jan 2015 -
MarkdownDocWritercode updated to include missingListIndexclass. - 20 Nov 2014 - Original article.