using fandoc
using compilerDoc
class Main {
Void main() {
tellMeAbout := "maps"
index := IndexBuilder().indexAll.buildIndex
index.tellMeAbout(tellMeAbout).join("\n\n" + "".padl(80, '-')) { it.toPlainText(120) } { echo(it) }
}
}
const class Index {
internal const Str:Section[] sections
new make(|This| f) { f(this) }
Section[] tellMeAbout(Str keyword) {
if (keyword.contains(" "))
throw ArgErr("Keywords can not contain whitespace! $keyword")
secs := (Section[]) (sections[keyword] ?: Section[,]).rw
stemmed := SectionBuilder.stem(keyword)
if (stemmed != keyword)
secs.addAll(sections[stemmed] ?: Section#.emptyList)
sortScore := |Section s->Int| { (s.parents.size * 2) + s.keywords.size + (s.isApi ? 10 : 0) }
secs = secs.rw.sort |s1, s2| { sortScore(s1) <=> sortScore(s2) }
return secs
}
}
class IndexBuilder {
private DocEnv docEnv := DefaultDocEnv()
Section[] sections := Section[,]
Index buildIndex() {
keywords := sections.map { it.keywords }.flatten.unique.sort
sections := Str:Section[][:]
keywords.each |keyword| {
sections[keyword] = this.sections.findAll { it.containsKeyword(keyword) }
}
return Index {
it.sections = sections
}
}
This indexAll() {
Env.cur.findAllPodNames.each { indexPod(it) }
return this
}
This indexPod(Str podName) {
podFile := Env.cur.findPodFile(podName)
docPod := DocPod.load(docEnv, podFile)
podSec := SectionBuilder(docPod)
sections.add(podSec.toSection)
indexDocs(podName, podSec)
indexTypes(docPod, podSec)
return this
}
This indexFandoc(Str pod, Str type, InStream in) {
doIndexFandoc(pod, type, in, null).map { it.toSection }
return this
}
private Void indexTypes(DocPod docPod, SectionBuilder podSec) {
docPod.types.each |DocType type| {
typeSec := SectionBuilder.makeType(type) { it.parents.push(podSec) }
secs := (SectionBuilder[]) type.slots.map {
SectionBuilder.makeSlot(it)
}
secs.each { it.parents.push(typeSec).push(podSec) }
sections.add(typeSec.toSection)
sections.addAll(secs.map { it.toSection })
}
}
private Void indexDocs(Str podName, SectionBuilder podSec) {
podFile := Env.cur.findPodFile(podName)
Zip.open(podFile).contents.findAll |file, uri| { uri.ext == "fandoc" && uri.path[0] == "doc" }.each |File fandocFile| {
typeSec := SectionBuilder.makeChapter(podName, fandocFile.basename) { it.parents.push(podSec) }
secs := doIndexFandoc(podName, fandocFile.basename, fandocFile.in, typeSec)
secs.each { it.parents.push(typeSec).push(podSec) }
sections.add(typeSec.toSection)
sections.addAll(secs.map { it.toSection })
}
}
private SectionBuilder[] doIndexFandoc(Str pod, Str type, InStream in, SectionBuilder? parent) {
doc := FandocParser().parse("${pod}::${type}", in, true)
overview := false
bobs := SectionBuilder[,]
// for now, ignore headings that are buried in lists
doc.children.each |elem| {
if (elem is Heading) {
if (parent != null && bobs.isEmpty && (elem as Heading).title == "Overview")
overview = true
else
bobs.add(SectionBuilder.makeDoc(pod, type, elem, bobs, overview))
} else {
// ? 'cos not all fandocs start with a heading!
if (bobs.isEmpty)
parent?.addContent(elem)
else
bobs.last.addContent(elem)
}
}
return bobs
}
}
const class Section {
const Str what
const Str pod
const Str? type
const Str title
const Str content
const Bool isApi
const Bool isDoc
const Str[] keywords
const Str fanUrl
const Uri webUrl
const Section[] parents
new make(|This| f) {
f(this)
// TODO add acronyms
keywords = keywords.map { it.lower }
}
internal Bool containsKeyword(Str keyword) {
keywords.contains(keyword)
}
Str toPlainText(Int maxWidth := 80) {
lev := 0
text := "\n\n(${what})\n${webUrl}\n\n"
parents.dup.insert(0, this).eachr {
text += "".justl(lev * 2)
text += "${it.title}\n"; lev++
}
text += "\n" + content
return "\n\n" + TextWrapper { normaliseWhitespace = false }.wrap(text, maxWidth)
}
@NoDoc
override Str toStr() { fanUrl }
}
class SectionBuilder {
static const Uri webBaseUrl := `http://f...content-available-to-author-only...m.org/doc/`
Str fanUrl
Uri webUrl
Str what
Str pod
Str? type
Str title
Str? fandoc
Bool isApi
Str[] keywords
Heading? heading
DocNode[]? content
SectionBuilder[] parents := SectionBuilder[,]
Section? section
new makePod(DocPod pod) {
this.what = "Pod"
this.pod = pod.name
this.title = pod.name
this.fandoc = pod.summary
this.fanUrl = "${pod.name}::index"
this.webUrl = webBaseUrl + `${pod.name}/index`
this.keywords = [pod.name]
}
new makeType(DocType type) {
this.what = "Type"
this.pod = type.pod.name
this.type = type.name
this.title = type.name
this.fanUrl = "${this.pod}::${this.type}"
this.webUrl = webBaseUrl + `${this.pod}/${this.type}`
this.fandoc = type.doc.text
this.keywords = [type.name]
this.isApi = true
}
new makeSlot(DocSlot slot) {
this.what = "Slot"
this.pod = slot.parent.pod
this.type = slot.parent.name
this.title = slot.name
this.fanUrl = "${this.pod}::${this.type}.${slot.name}"
this.webUrl = webBaseUrl + `${this.pod}/${this.type}#${slot.name}`
this.fandoc = slot.doc.text
this.keywords = [slot.name]
this.isApi = true
field := slot as DocField
if (field != null) {
this.what = "Field"
if (field.init != null)
title += " := ${field.init}"
}
method := slot as DocMethod
if (method != null) {
this.what = "Method"
title += "(" + method.params.join(", ") { it.toStr } + ")"
}
}
new makeChapter(Str pod, Str type) {
this.what = "Documentation"
if (type == "pod") {
this.pod = pod
this.type = "pod-doc"
this.title = "pod-doc"
this.fanUrl = "${pod}::index"
this.webUrl = webBaseUrl + `${pod}/index`
this.content = DocNode[,]
this.keywords = [pod]
} else {
this.pod = pod
this.type = type
this.title = type
this.fanUrl = "${pod}::${type}"
this.webUrl = webBaseUrl + `${pod}/${type}`
this.content = DocNode[,]
this.keywords = type.toDisplayName.split.map { stem(it) }
}
}
new makeDoc(Str pod, Str type, Heading heading, SectionBuilder[] bobs, Bool overview) {
this.what = "Documentation"
this.pod = pod
this.type = type
this.heading = heading
this.fanUrl = "${pod}::${type}#${heading.anchorId}"
this.webUrl = webBaseUrl + `${pod}/${type}#${heading.anchorId}`
this.content = DocNode[,]
levs := Int[1]
lev := heading.level
bobs.eachr |sec| {
if (sec.heading.level == lev)
levs.push(levs.pop.increment)
if (sec.heading.level < lev) {
levs.push(1)
lev = sec.heading.level
parents.push(sec)
}
}
// cater for missing out 'Overview' sections
if (overview)
levs.push(levs.pop.increment)
chapter := Version(levs.reverse)
this.title = "${chapter}. ${heading.title}"
this.keywords = heading.title.toDisplayName.split.map { stem(it) }
.exclude |Str key->Bool| { key.size < 2 || key.endsWith("-") } // remove nonsense
.exclude |Str key->Bool| { ["and", "or", "the"].contains(key) } // remove stopwords
}
// TODO proper stemming!
static Str stem(Str word) {
// classes -> class, closures -> closure!!!??
if (word.endsWith("ses"))
word = word[0..<-2]
// pods -> pod, this -> this, class -> class
if (word.endsWith("s") && !word.endsWith("is") && !word.endsWith("ss"))
word = word[0..<-1]
return word
}
Void addContent(DocNode node) {
content.add(node)
}
Section toSection() {
if (fandoc == null) {
buf := Buf()
out := FandocDocWriter(buf.out)
content.each { it.write(out) }
fandoc = buf.flip.readAllStr
}
return section = Section {
it.what = this.what
it.pod = this.pod
it.type = this.type
it.title = this.title
it.isApi = this.isApi
it.isDoc = this.isApi.not
it.keywords = this.keywords
it.content = fandoc
it.fanUrl = this.fanUrl
it.webUrl = this.webUrl
it.parents = this.parents.map { it.section }.exclude { it == null }
}
}
override Str toStr() { fanUrl }
}
** Utility class for pretty-printing text.
**
** Example:
**
** pre>
** text := TextWrapper().wrap("Chuck Norris once ordered a Big Mac at Burger King, and got one.", 25)
**
** // --> Chuck Norris once ordered
** a Big Mac at Burger King,
** and got one.
**
** <pre
const class TextWrapper {
** If 'true', then the text is trimmed.
const Bool trim := true
** If 'true', then each run of whitespace (including tabs and new lines) is replaced with a single space character.
const Bool normaliseWhitespace := true
** If 'true', then words longer than 'maxWidth' will be broken in order to ensure that no line
** is longer than 'maxWidth'.
**
** If 'false', then long words will not be broken and some lines may be longer than 'maxWidth'.
** (Long words will be put on a line by themselves, in order to minimise the amount by which
** 'maxWidth' is exceeded.)
const Bool breakLongWords := true
** If 'true', wrapping will occur on whitespace and after hyphens in compound words.
const Bool breakOnHyphens := true
** Standard it-block ctor for setting field values.
**
** syntax: fantom
** iceT := TextWrapper {
** it.breakLongWords = false
** }
new make(|This|? f := null) { f?.call(this) }
** Formats and word wraps the given text.
Str wrap(Str text, Int maxWidth) {
if (trim)
text = text.trim
if (normaliseWhitespace) {
chrs := Int[,]
spce := false
text.each |ch| {
if (ch.isSpace) {
if (!spce)
chrs.add(' ')
spce = true
} else {
spce = false
chrs.add(ch)
}
}
text = Str.fromChars(chrs)
}
buff := StrBuf()
line := StrBuf()
word := StrBuf()
flushLine := |->| {
if (!normaliseWhitespace || line.toStr.trimEnd.size > 0) {
buff.join(line.toStr.trimEnd, "\n")
line.clear
}
}
flushWord := |Str char| {
if (word.size + char.size > 0) {
if (line.size + (word.toStr + char).trim.size > maxWidth)
flushLine()
if (word.size > maxWidth && breakLongWords) {
flushLine()
while (word.size > maxWidth) {
part := word.getRange(0..<maxWidth)
buff.join(part, "\n")
word.removeRange(0..<maxWidth)
}
}
line.add(word.toStr)
word.clear
if (char == "\n")
flushLine()
else
line.add(char)
}
}
text.each |char| {
if (char.isSpace || (breakOnHyphens && char == '-'))
flushWord(char.toChar)
else
word.addChar(char)
}
flushWord("")
flushLine()
return buff.toStr
}
}