//
// Copyright (c) 2011, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 11 Aug 11 Brian Frank Creation
//
**
** ApiDocParser is used to parse the text file syntax of the
** apidoc file generated by the compiler. These files are
** designed to give us full access everything we need to build
** a documentation model of pods, types, and slots using a
** simple human readable format.
**
** The syntax is defined as:
** <file> := <class> <slot>*
** <class> := "== " <name> <nl> <attrs>
** <slot> := (<fieldSig> | <methodSig>) <attrs>
** <fieldSig> := "-- " <name> <sp> <type> [":=" <expr>] <nl>
** <methodSig> := "-- " <name> "(" <nl> [<param> <nl>]* ")" <sp> <return> <nl>
** <param> := <name> <type> [":=" <expr>]
** <return> := <type>
**
** <attrs> := <meta>* <facet>* <nl> <doc>.
** <meta> := <name> "=" <expr> <nl>
** <facet> := "@" <type> ["{" <nl> [<name> "=" <expr> <nl>]* "}"] <nl>
** <doc> := lines of text until "-- "
**
** <name> := Fantom identifier
** <type> := Fantom type signature (no spaces allowed)
** <expr> := text until end of line
** <nl> := "\n"
** <sp> := " "
**
** Standard attributes:
** - base: list of space separated base class type signatures
** - mixins: list of space separated mixin type signatures
** - loc: <file> ":" <line> "/" <docLine>
** - flags: type or slot space separated flag keywords
** - set: field setter flags
**
** Note that the grammar is defined such that expr to display
** in docs for field and parameter defaults is always positioned at
** the end of the line (avoiding nasty escaping problems).
**
internal class ApiDocParser
{
new make(DocPod pod, InStream in)
{
this.pod = pod
this.in = in
consumeLine
}
DocType parseType(Bool close := true)
{
try
{
// == <name>
if (!cur.startsWith("== ")) throw Err("Expected == <name>")
name := cur[3..-1]
consumeLine
// parse attrs
attrs := parseAttrs
this.typeRef = DocTypeRef("${pod.name}::${name}")
this.typeLoc = attrs.loc
// zero or more slots
list := DocSlot[,]
map := Str:DocSlot[:]
while (true)
{
slot := parseSlot
if (slot == null) break
list.add(slot)
map[slot.name] = slot
}
// construct DocType from my own fields
return DocType(pod, attrs, typeRef, list, map)
}
finally { if (close) in.close }
}
private DocSlot? parseSlot()
{
// check if at end of file
if (cur.isEmpty) return null
// "-- " <name> <sp> <type> [":=" <expr>]
// "-- " <name> "(" <nl> [<param> <nl>]* ")" <return>
if (!cur.startsWith("-- ")) throw Err("Expected -- <name>")
if (cur[-1] == '(')
return parseMethod
else
return parseField
}
private DocField parseField()
{
// "-- " <name> <sp> <type> [":=" <expr>]
sp := cur.index(" ", 4)
initi := cur.index(":=", sp+1)
name := cur[3..<sp]
type := cur[sp+1 ..< (initi ?: cur.size)]
init := initi == null ? null : cur[initi+2..-1]
consumeLine
attrs := parseAttrs
return DocField(attrs, typeRef, name, DocTypeRef(type), init)
}
private DocMethod parseMethod()
{
// "-- " <name> "(" <nl> [<param> <nl>]* ")" <return>
// tokenize by space
name := cur[3..-2]
consumeLine
// parse params
params := DocParam[,]
while (cur[0] != ')')
{
sp := cur.index(" ")
defi := cur.index(":=", sp+1)
pname := cur[0..<sp]
type := cur[sp+1 ..< (defi ?: cur.size)]
def := defi == null ? null : cur[defi+2..-1]
params.add(DocParam(DocTypeRef(type), pname, def))
consumeLine
}
returns := DocTypeRef(cur[2..-1])
consumeLine
// attrs, facets, and doc
attrs := parseAttrs
attrs.flags = attrs.flags.and(DocFlags.Const.not)
return DocMethod(attrs, typeRef, name, returns, params)
}
** Parse meta name/val pairs, facets, and fandoc section
private DocAttrs parseAttrs()
{
attrs := DocAttrs()
parseMeta(attrs)
parseFacets(attrs)
parseDoc(attrs)
return attrs
}
private Void parseMeta(DocAttrs attrs)
{
while (!cur.isEmpty && cur[0].isAlpha)
{
eq := cur.index("=")
name := cur[0..<eq]
val := cur[eq+1..-1]
switch (name)
{
case "loc": parseLoc(attrs, val)
case "flags": attrs.flags = DocFlags.fromNames(val)
case "base": attrs.base = parseTypeList(val)
case "mixins": attrs.mixins = parseTypeList(val)
case "set": attrs.setterFlags = DocFlags.fromNames(val)
}
consumeLine
}
}
private Void parseLoc(DocAttrs attrs, Str val)
{
colon := val.index(":")
slash := val.indexr("/")
file := colon == 0 ? this.typeLoc.file : val[0..<colon]
line := val[colon+1 ..< (slash ?: val.size)].toInt
docLine := slash != null ? val[slash+1..-1].toInt : line
attrs.loc = DocLoc(file, line)
attrs.docLoc = DocLoc(file, docLine)
}
private DocTypeRef[] parseTypeList(Str val)
{
val.split.map |tok->DocTypeRef| { DocTypeRef(tok) }
}
private Void parseFacets(DocAttrs attrs)
{
facet := parseFacet
if (facet == null) return
acc := [facet]
while ((facet = parseFacet) != null) acc.add(facet)
attrs.facets = acc
}
private DocFacet? parseFacet()
{
if (!cur.startsWith("@")) return null
complex := cur[-1] == '{'
type := DocTypeRef(cur[1..(complex ? -2 : -1)])
fields := DocFacet.noFields
consumeLine
if (complex)
{
fields = Str:Str[:]
fields.ordered = true
while (cur != "}")
{
eq := cur.index("=")
name := cur[0..<eq]
val := cur[eq+1..-1]
fields[name] = val
consumeLine
}
consumeLine // trailing "}"
}
return DocFacet(type, fields)
}
private Void parseDoc(DocAttrs attrs)
{
if (!cur.isEmpty) throw Err("expecting empty line")
consumeLine
s := StrBuf(256)
while (!eof && !cur.startsWith("-- "))
{
s.add(cur).addChar('\n')
consumeLine
}
attrs.doc = DocFandoc(attrs.docLoc, s.toStr)
}
private Void consumeLine()
{
next := in.readLine
if (next != null) cur = next
else { cur = ""; eof = true }
}
private InStream in
private const DocPod pod
private Str cur := ""
private DocLoc typeLoc := DocLoc.unknown
private DocTypeRef? typeRef
private Bool eof
}
internal class DocAttrs
{
Int flags
DocLoc loc := DocLoc.unknown
DocLoc docLoc := DocLoc.unknown
Int? setterFlags
DocTypeRef[] base := DocTypeRef#.emptyList
DocTypeRef[] mixins := DocTypeRef#.emptyList
DocFacet[] facets := DocFacet#.emptyList
DocFandoc? doc
}