//
// Copyright (c) 2007, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 19 Feb 07 Brian Frank Creation
//
**
** InlineParser parses a block of formatted text into
** a series of inline elements.
**
@Js
internal class InlineParser
{
//////////////////////////////////////////////////////////////////////////
// Parser
//////////////////////////////////////////////////////////////////////////
**
** Constructor takes a closure which feeds characters.
**
new make(FandocParser parent, StrBuf src, Int startLine)
{
this.parent = parent
this.src = src
this.stack = DocNode[,]
this.line = startLine
// initialize cur and peek
last = ' '
cur = peek = -1
if (src.size > 0) cur = src[0]
if (src.size > 1) peek = src[1]
if (cur == '\n') { ++line; cur= ' ' }
if (peek == '\n') { ++line; peek = ' ' }
pos = 0
}
//////////////////////////////////////////////////////////////////////////
// Block
//////////////////////////////////////////////////////////////////////////
Void parse(DocElem parent)
{
while (cur > 0)
segment(parent)
}
private Void segment(DocElem parent)
{
stack.push(parent)
DocNode? child
if (last.isSpace || last == '*' || last == '/' || last == '(')
{
switch (cur)
{
case '\'': child = code
case '`': child = link
case '[': child = annotation(parent)
case '*': child = (peek == '*') ? strong : emphasis
case '!': child = (peek == '[') ? image : text
default: child = text
}
}
else
{
child = text
}
if (child != null) parent.add(child)
stack.pop
}
private Bool isTextEnd()
{
switch (cur)
{
// these characters always indicate a new segment
// if preceeded by a space because they can't contain
// embedded segments
case '\'':
case '`':
case '[':
return last.isSpace || last == '('
// ![
case '!':
return peek == '[' && last.isSpace
// check for end of emphasis/strong or start of new one
case '*':
if (stack.peek.id == DocNodeId.strong)
// if inside a strong, then end of strong, or start of emphasis
// ends the current text.
return peek == '*' || last.isSpace
else if (stack.peek.id == DocNodeId.emphasis)
return true
else
return last.isSpace
default:
return false
}
}
private DocText text()
{
buf := StrBuf.make
buf.addChar(cur)
consume
while (cur > 0 && !isTextEnd)
{
buf.addChar(cur)
consume
}
return DocText(buf.toStr)
}
private DocNode code()
{
buf := StrBuf.make
consume
while (cur != '\'')
{
if (cur <= 0) throw err("Invalid code")
buf.addChar(cur)
consume
}
consume
code := Code.make
code.add(DocText(buf.toStr))
return code
}
private DocNode emphasis()
{
if (peek <= 0 || peek.isSpace && peekPeek != '*')
return text
em := Emphasis.make
consume
while (cur != '*' || peek == '*')
{
if (cur <= 0) throw err("Invalid *emphasis*")
segment(em)
}
consume
return em
}
private DocNode strong()
{
strong := Strong.make
consume
consume
while (cur != '*' || peek != '*')
{
if (cur <= 0) throw err("Invalid **strong**")
segment(strong)
}
consume
consume
return strong
}
private DocNode link()
{
link := Link(uri)
link.line = this.line
link.add(DocText(link.uri))
return link
}
private DocNode? annotation(DocElem parent)
{
if (peek <= 0 || peek == ']')
return text
// there are three options for square brackets
// [anchor]`url` // hyperlink
// [![alt]`image`]`url` // image hyperlink (no spaces allowed)
// [#frag] // id to link to a heading
DocNode? body
Str? anchor
if (peek == '!' && peekPeek == '[')
{
consume // [
body = image
if (cur != ']') throw err("Invalid img link")
consume // ]
}
else
{
s := brackets
if (s.startsWith("#"))
{
parent.anchorId = s[1..-1]
return null
}
body = DocText(s)
}
if (cur == '`')
{
link := Link(uri)
link.add(body)
return link
}
else
{
throw err("Invalid annotation []")
}
}
private DocNode image()
{
consume // !
alt := brackets
size := null
if (cur == '[') size = brackets
uri := uri
img := Image(uri, alt)
img.size = size
img.line = this.line
return img
}
private Str uri()
{
if (cur != '`') throw err("Invalid uri")
consume // leading `
buf := StrBuf.make
while (cur != '`')
{
if (cur <= 0) throw err("Invalid uri")
buf.addChar(cur)
consume
}
consume // trailing `
return buf.toStr
}
private Str brackets()
{
if (cur != '[') throw err("Invalid []")
consume // leading [
buf := StrBuf.make
while (cur != ']')
{
if (cur <= 0) throw err("Invalid []")
buf.addChar(cur)
consume
}
consume // leading ]
return buf.toStr
}
////////////////////////////////////////////////////////////////
// Utils
////////////////////////////////////////////////////////////////
**
** Make exception to terminate processing.
**
Err err(Str msg)
{
return FandocErr(msg, parent.filename, line)
}
**
** Consume the cur char and advance to next char in buffer:
** - updates cur and peek fields
** - end of file, sets fields to null
**
private Void consume()
{
last = cur
cur = peek
pos++
if (pos+1 < src.size)
{
peek = src[pos+1] // next peek is cur+1
if (peek == '\n') { ++line; peek = ' '; }
}
else
{
peek = -1
}
}
**
** Look at char after peek
**
private Int peekPeek()
{
if (pos+2 < src.size) return src[pos+2]
return -1
}
private Str debug()
{
"cur='" + (cur <= 0 ? "eof" : cur.toChar) +
"' peek='" + (peek <= 0 ? "eof" : peek.toChar) + "'"
}
//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////
private FandocParser parent // parent parser
private StrBuf src // characters to parse
private Int line // line
private Int pos // index into buf for cur
private Int last // last char
private Int cur // current char
private Int peek // next char
private DocNode[] stack // stack of nodes
}