:: 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#c14431sorted := [:] { it.ordered = true } unordered.keys.sortr.each { sorted[it] = unordered[it] } return sorted } }
Have fun!
Edits:
- 31 Jul 2016 - Deprecated.
- 6 Jan 2015 -
MarkdownDocWriter
code updated to include missingListIndex
class. - 20 Nov 2014 - Original article.