|
| 1 | +// |
| 2 | +// Copyright (c) 2026, Brian Frank and Andy Frank |
| 3 | +// Licensed under the Academic Free License version 3.0 |
| 4 | +// |
| 5 | +// History: |
| 6 | +// 5 Mar 2026 Brian Frank Creation |
| 7 | +// |
| 8 | + |
| 9 | +** |
| 10 | +** XetodocChapter is a simple DOM for the chapter meta and headings tree |
| 11 | +** that can be used b/w compilerDoc and xetodoc |
| 12 | +** |
| 13 | +@Js @NoDoc |
| 14 | +class XetodocChapter |
| 15 | +{ |
| 16 | + ** Parse a markdown file as a chapter into its meta and headings |
| 17 | + static XetodocChapter parse(Str file) |
| 18 | + { |
| 19 | + meta := Str:Str[:] { ordered = true } |
| 20 | + list := XetodocHeading[,] |
| 21 | + map := Str:XetodocHeading[:] |
| 22 | + |
| 23 | + // read lines |
| 24 | + lines := file.splitLines |
| 25 | + |
| 26 | + // check leading comment for title: xxxx |
| 27 | + if (lines.first.trim == "<!--") |
| 28 | + { |
| 29 | + lines.eachWhile |line| |
| 30 | + { |
| 31 | + line = line.trim |
| 32 | + if (line == "-->") return "break" |
| 33 | + colon := line.index(":") |
| 34 | + if (colon == null) return null |
| 35 | + n := line[0..<colon].trim |
| 36 | + v := line[colon+1..-1].trim |
| 37 | + meta[n] = v |
| 38 | + return null |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + // lazily parse just heading lines |
| 43 | + proc := HeadingProcessor() |
| 44 | + l := XetodocHeading[,] |
| 45 | + lines.each |line| |
| 46 | + { |
| 47 | + if (!line.startsWith("#")) return |
| 48 | + |
| 49 | + // compute level |
| 50 | + level := 0 |
| 51 | + while (level+1 < line.size && line[level] == '#') level++ |
| 52 | + |
| 53 | + // create heading instance |
| 54 | + text := line[level..-1].trim |
| 55 | + anchor := proc.toAnchor(text) |
| 56 | + h := XetodocHeading(level, text, anchor) |
| 57 | + |
| 58 | + // add to accumulator collections |
| 59 | + list.add(h) |
| 60 | + map[h.anchor] = h |
| 61 | + } |
| 62 | + |
| 63 | + // now organize into a tree |
| 64 | + top := XetodocHeading[,] |
| 65 | + list.each |h, i| |
| 66 | + { |
| 67 | + if (i == 1) { top.add(h); return } |
| 68 | + for (j := i-1; j >= 0; --j) |
| 69 | + { |
| 70 | + if (list[j].level < h.level) |
| 71 | + { |
| 72 | + parent := list[j] |
| 73 | + parent.children.add(h) |
| 74 | + return |
| 75 | + } |
| 76 | + } |
| 77 | + top.add(h) |
| 78 | + } |
| 79 | + |
| 80 | + return make { |
| 81 | + it.meta = meta |
| 82 | + it.top = top |
| 83 | + it.byId = map |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + private new make(|This| f) { f(this) } |
| 88 | + |
| 89 | + Str:Str meta |
| 90 | + |
| 91 | + Str? title() { meta["title"] } |
| 92 | + |
| 93 | + Str:Str anchorToTextMap() { byId.map |h->Str| { h.text } } |
| 94 | + |
| 95 | + XetodocHeading[] top |
| 96 | + |
| 97 | + Str:XetodocHeading byId |
| 98 | + |
| 99 | + Void dump() |
| 100 | + { |
| 101 | + echo("#####") |
| 102 | + echo(meta.join("\n")) |
| 103 | + echo("---") |
| 104 | + top.each |x| { x.dump(0) } |
| 105 | + } |
| 106 | + |
| 107 | + static Void main(Str[] args) |
| 108 | + { |
| 109 | + parse(args[0].toUri.toFile.readAllStr).dump |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +************************************************************************** |
| 114 | +** XetodocHeading |
| 115 | +************************************************************************** |
| 116 | + |
| 117 | +@Js @NoDoc |
| 118 | +class XetodocHeading |
| 119 | +{ |
| 120 | + new make(Int level, Str text, Str anchor) |
| 121 | + { |
| 122 | + this.level = level |
| 123 | + this.text = text |
| 124 | + this.anchor = anchor |
| 125 | + } |
| 126 | + |
| 127 | + const Int level |
| 128 | + const Str text |
| 129 | + const Str anchor |
| 130 | + XetodocHeading[] children := [,] |
| 131 | + |
| 132 | + override Str toStr() { "$level | $text.toCode [#$anchor]" } |
| 133 | + |
| 134 | + Void dump(Int indent) |
| 135 | + { |
| 136 | + echo(Str.spaces(indent) + this) |
| 137 | + children.each |kid| { kid.dump(indent+2) } |
| 138 | + } |
| 139 | +} |
| 140 | + |
0 commit comments