using fandoc
using fandoc::DocWriter
//
// Copyright (c) 2014, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 5 May 14 Steve Eynon Creation
//
** Same as the official 'FandocDocWriter', but with bug fixes! See "SlimerDude" comments in source.
**
** 'FandocDocWriter' outputs a fandoc model to plain text fandoc format
**
@Js
class FandocDocWriter : DocWriter
{
new make(OutStream out)
{
this.out = out
}
** Callback to perform link resolution and checking for
** every Link element
|Link link|? onLink := null
** Callback to perform image link resolution and checking
|Image img|? onImage := null
override Void docStart(Doc doc)
{
if (doc.meta.isEmpty)
{
out.printLine
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
onImage?.call(img)
out.print("![${img.alt}")
if (img.size != null) out.print("][${img.size}")
case DocNodeId.para:
// for when a para follows a link
out.printLine
out.printLine // SlimerDude - I added this!
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.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
case DocNodeId.hr:
out.print("---\n\n")
}
}
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
size := head.title.size
if (head.anchorId != null)
{
out.print(" [#${head.anchorId}]")
size += head.anchorId.size + 4
}
char := "#*=-".chars[head.level-1]
line := Str.defVal.padl(size.max(3), char)
out.printLine.printLine(line)
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
indentNo := (listIndexes.size + 1) * 2
indent := Str.defVal.padl(indentNo)
text.str.splitLines.each
{
out.print(indent).printLine(it)
}
} else {
out.printLine("pre>")
out.print(text.str)
out.printLine("<pre")
}
out.printLine
}
else out.print(text.str)
}
private OutStream out
private ListIndex[] listIndexes := [,]
private Bool inBlockquote
private Bool inPre
private Bool inListItem
}
**************************************************************************
** ListIndex
**************************************************************************
@Js
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
}
}