//
// Copyright (c) 2007, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 17 Feb 07 Brian Frank Creation
//
**************************************************************************
** DocNodeId
**************************************************************************
@Js
enum class DocNodeId
{
text,
doc,
heading,
para,
pre,
blockQuote,
orderedList,
unorderedList,
listItem,
emphasis,
strong,
code,
link,
image,
hr
}
**************************************************************************
** Node
**************************************************************************
**
** DocNode is the base class for nodes in a fandoc model.
** There are two type of nodes: DocElem and DocText.
**
** See [pod doc]`pod-doc#api` for usage.
**
@Js
abstract class DocNode
{
**
** Get node id for node type.
**
abstract DocNodeId id()
**
** Write this node to the specified DocWriter.
**
abstract Void write(DocWriter out)
**
** Is this an inline versus a block node.
**
abstract Bool isInline()
**
** Is this a block element versus an inline element.
**
Bool isBlock() { return !isInline }
**
** Debug dump to output stream.
**
Void dump(OutStream out := Env.cur.out)
{
html := HtmlDocWriter(out)
write(html)
html.out.flush
}
//////////////////////////////////////////////////////////////////////////
// Path Utilities
//////////////////////////////////////////////////////////////////////////
**
** Get the `DocElem` that contains this node.
** Return 'null' if not parented.
**
DocElem? parent { internal set }
**
** Get the path from the root of the DOM to this node.
**
virtual DocNode[] path()
{
DocNode[] p := [this]
cur := parent
while (cur != null)
{
p.add(cur)
cur = cur.parent
}
return p.reverse
}
**
** Get the index of this node in its parent's children.
** Return 'null' if not parented.
**
Int? pos()
{
return parent?.children?.indexSame(this)
}
**
** Return 'true' if this node is the first child in its parent.
**
Bool isFirst()
{
return pos == 0
}
**
** Return 'true' if this node is the last child in its parent.
**
Bool isLast()
{
return parent?.children?.last === this
}
**
** Get all the DocText children as a string
**
abstract Str toText()
}
**************************************************************************
** DocText
**************************************************************************
**
** DocText segment.
**
** See [pod doc]`pod-doc#api` for usage.
**
@Js
class DocText : DocNode
{
new make(Str str) { this.str = str }
override DocNodeId id() { return DocNodeId.text }
override Void write(DocWriter out)
{
out.text(this)
}
override Bool isInline() { true }
override Str toText() { str }
override Str toStr() { str }
Str str
}
**************************************************************************
** DocElem
**************************************************************************
**
** DocElem is a container node which models a branch of the doc tree.
**
** See [pod doc]`pod-doc#api` for usage.
**
@Js
abstract class DocElem : DocNode
{
**
** Get the HTML element name to use for this element.
**
abstract Str htmlName()
**
** Write this element and its children to the specified DocWriter.
**
override Void write(DocWriter out)
{
out.elemStart(this)
writeChildren(out)
out.elemEnd(this)
}
**
** Write this element's children to the specified DocWriter.
**
Void writeChildren(DocWriter out)
{
children.each |DocNode child| { child.write(out) }
}
//////////////////////////////////////////////////////////////////////////
// Children
//////////////////////////////////////////////////////////////////////////
**
** Get a readonly list of this elements's children.
**
DocNode[] children() { return kids.ro }
**
** Iterate the children nodes
**
Void eachChild(|DocNode| f) { kids.each(f) }
@Deprecated { msg = "Use add()" }
This addChild(DocNode node) { add(node) }
**
** Add a child to this node. If adding a text node
** it is automatically merged with the trailing text
** node (if applicable). If the node is arlready parented
** thorw ArgErr. Return this.
**
@Operator This add(DocNode node)
{
if (node.parent != null) throw ArgErr("Node already parented: $node")
if (!kids.isEmpty)
{
last := kids.last
// if adding two text nodes, then merge them
if (node.id === DocNodeId.text && last.id === DocNodeId.text)
{
((DocText)kids.last).str += ((DocText)node).str
return this
}
// two consecutive blockquotes get merged
if (node.id === DocNodeId.blockQuote && last.id == DocNodeId.blockQuote)
{
DocElem elem := (DocElem)node
elem.kids.dup.each |child| { elem.remove(child); last->addChild(child) }
return this
}
}
node.parent = this
kids.add(node)
return this
}
**
** Insert a child node at the specified index. A negative index may be
** used to access an index from the end of the list. If adding a text node
** it is automatically merged with surrounding text nodes (if applicable).
** If the node is already parented throws ArgErr.
**
This insert(Int index, DocNode node)
{
tail := DocNode[node]
kids.dup.eachRange(index..-1) |child| { remove(child); tail.add(child) }
tail.each { add(it) }
return this
}
**
** Convenicence to call `add` for each node in the given list.
**
This addAll(DocNode[] nodes)
{
nodes.each |node| { add(node) }
return this
}
**
** Remove a child node. If this element is not the child's
** current parent throw ArgErr. Return this.
**
This remove(DocNode node)
{
if (kids.removeSame(node) == null) throw ArgErr("not my child: $node")
node.parent = null
return this
}
**
** Remove all child nodes. Return this.
**
This removeAll()
{
kids.dup.each |node| { remove(node) }
return this
}
**
** Get all the DocText children as a string
**
override Str toText()
{
if (kids.size == 1) return kids.first.toText
s := StrBuf()
kids.each |kid| { s.join(kid.toText, " ") }
return s.toStr
}
//////////////////////////////////////////////////////////////////////////
// Path
//////////////////////////////////////////////////////////////////////////
**
** Covariant override to narrow path to list of `DocElem`.
**
final override DocElem[] path()
{
return super.path.map|n->DocElem| { (DocElem)n }
}
//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////
private DocNode[] kids := [,]
Str? anchorId
}
**************************************************************************
** Doc
**************************************************************************
**
** Doc models the top level node of a fandoc document.
**
@Js
class Doc : DocElem
{
override DocNodeId id() { return DocNodeId.doc }
override Str htmlName() { return "body" }
override Bool isInline() { return false }
override Void write(DocWriter out)
{
out.docStart(this)
super.write(out)
out.docEnd(this)
}
**
** Recursively walk th document to build an order list of the
** multi-level headings which can serve as a "table of contents"
** for the document.
**
Heading[] findHeadings()
{
acc := Heading[,]
doFindHeadings(acc, this)
return acc
}
private Void doFindHeadings(Heading[] acc, DocElem elem)
{
if (elem is Heading) acc.add(elem)
elem.children.each |kid| { if (kid is DocElem) doFindHeadings(acc, kid) }
}
Str:Str meta := Str:Str[:]
}
**************************************************************************
** Heading
**************************************************************************
**
** Heading
**
@Js
class Heading : DocElem
{
new make(Int level) { this.level = level }
override DocNodeId id() { return DocNodeId.heading }
override Str htmlName() { return "h$level" }
override Bool isInline() { return false }
Str title() { toText }
const Int level
}
**************************************************************************
** Para
**************************************************************************
**
** Para models a paragraph of text.
**
@Js
class Para : DocElem
{
override DocNodeId id() { return DocNodeId.para }
override Str htmlName() { return "p" }
override Bool isInline() { return false }
Str? admonition // WARNING, NOTE, TODO, etc
}
**************************************************************************
** Pre
**************************************************************************
**
** Pre models a pre-formated code block.
**
@Js
class Pre : DocElem
{
override DocNodeId id() { return DocNodeId.pre }
override Str htmlName() { return "pre" }
override Bool isInline() { return false }
}
**************************************************************************
** BlockQuote
**************************************************************************
**
** BlockQuote models a block of quoted text.
**
@Js
class BlockQuote : DocElem
{
override DocNodeId id() { return DocNodeId.blockQuote }
override Str htmlName() { return "blockquote" }
override Bool isInline() { return false }
}
**************************************************************************
** OrderedList
**************************************************************************
**
** OrderedList models a numbered list
**
@Js
class OrderedList : DocElem
{
new make(OrderedListStyle style) { this.style = style }
override DocNodeId id() { return DocNodeId.orderedList }
override Str htmlName() { return "ol" }
override Bool isInline() { return false }
OrderedListStyle style
}
**
** OrderedListStyle
**
@Js
enum class OrderedListStyle
{
number, // 1, 2, 3, 4
upperAlpha, // A, B, C, D
lowerAlpha, // a, b, c, d
upperRoman, // I, II, III, IV
lowerRoman // i, ii, iii, iv
Str htmlType()
{
switch (this)
{
case number: return "decimal"
case upperAlpha: return "upper-alpha"
case lowerAlpha: return "lower-alpha"
case upperRoman: return "upper-roman"
case lowerRoman: return "lower-roman"
default: throw Err(toStr)
}
}
static OrderedListStyle fromFirstChar(Int ch)
{
if (ch == 'I') return upperRoman
if (ch == 'i') return lowerRoman
if (ch.isUpper) return upperAlpha
if (ch.isLower) return lowerAlpha
return number
}
}
**************************************************************************
** UnorderedList
**************************************************************************
**
** UnorderedList models a bullet list
**
@Js
class UnorderedList : DocElem
{
override DocNodeId id() { return DocNodeId.unorderedList }
override Str htmlName() { return "ul" }
override Bool isInline() { return false }
}
**************************************************************************
** ListItem
**************************************************************************
**
** ListItem is an item in an OrderedList and UnorderedList.
**
@Js
class ListItem : DocElem
{
override DocNodeId id() { return DocNodeId.listItem }
override Str htmlName() { return "li" }
override Bool isInline() { return false }
}
**************************************************************************
** Emphasis
**************************************************************************
**
** Emphasis is italic text
**
@Js
class Emphasis : DocElem
{
override DocNodeId id() { return DocNodeId.emphasis }
override Str htmlName() { return "em" }
override Bool isInline() { return true }
}
**************************************************************************
** Strong
**************************************************************************
**
** Strong is bold text
**
@Js
class Strong : DocElem
{
override DocNodeId id() { return DocNodeId.strong }
override Str htmlName() { return "strong" }
override Bool isInline() { return true }
}
**************************************************************************
** Code
**************************************************************************
**
** Code is inline code
**
@Js
class Code : DocElem
{
override DocNodeId id() { return DocNodeId.code }
override Str htmlName() { return "code" }
override Bool isInline() { return true }
}
**************************************************************************
** Link
**************************************************************************
**
** Link is a hyperlink.
**
@Js
class Link : DocElem
{
new make(Str uri) { this.uri = uri }
override DocNodeId id() { return DocNodeId.link }
override Str htmlName() { return "a" }
override Bool isInline() { return true }
** Is the text of the link the same as the URI string
Bool isTextUri() { children.first is DocText && children.first.toStr == this.uri }
** Change the text to display for the link
Void setText(Str text) { removeAll.add(DocText(text)) }
Bool isCode := false // when uri resolves to a type or slot
Str uri
Int line
}
**************************************************************************
** Image
**************************************************************************
**
** Image is a reference to an image file
**
@Js
class Image : DocElem
{
new make(Str uri, Str alt) { this.uri = uri; this.alt = alt }
override DocNodeId id() { return DocNodeId.image }
override Str htmlName() { return "img" }
override Bool isInline() { return true }
Str uri
Str alt
Str? size // formatted {w}x{h}
Int line
}
**************************************************************************
** Hr
**************************************************************************
**
** Hr models a horizontal rule.
**
@Js
class Hr : DocElem
{
override DocNodeId id() { return DocNodeId.hr }
override Str htmlName() { return "hr" }
override Bool isInline() { return false }
}