:: Article Deprecated!

:: Fantom 1.0.68 now ships with it's own MarkdownDocWriter class.

:: Article Deprecated!

Markdown 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:

select all
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:

select all
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 - MarkdownDocWriter code updated to include missing ListIndex class.
  • 20 Nov 2014 - Original article.

Discuss