
Format.pudu
Pudu147 lines6.0 KB
1/** @Docgen.Site.Format — published HTML laid out with two-space indentation */2module PuduLangDocgen.Site.Format34import Std.Map as Map5import Std.Set as Set67/** @Docgen.Site.Piece — a tag, text, or verbatim run of an HTML document */8type Piece = { kind: Str, name: Str, text: Str }91011const BLOCKS: Set[Str] = setOf([12 "html", "head", "body", "header", "footer", "main", "nav", "aside", "section", "article", "div", "ul", "ol",13 "li", "dl", "dt", "dd", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption", "colgroup", "col",14 "h1", "h2", "h3", "h4", "h5", "h6", "p", "figure", "figcaption", "details", "summary", "blockquote", "form",15 "fieldset", "meta", "link", "title", "hr", "noscript", "iframe", "video", "audio", "source", "picture", "urlset",16 "url", "loc", "lastmod", "changefreq", "priority", "base"17 ])181920const VERBATIM: Set[Str] = setOf(["pre", "script", "style", "textarea"])212223const VOID: Set[Str] = setOf(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"])242526const INDENT: Str = " "27282930export fn html(document: Str) -> Str {31 let pieces = split(document)32 var closes: Map[Int, Int] = mapOf([])33 var containsBlocks: Set[Int] = setOf([])34 var stack: Array[Int] = []35 for index in 0..pieces.length() {36 let piece = pieces[index]37 let block = Set.contains(&BLOCKS, piece.name)38 if piece.kind == "raw" || piece.kind == "doctype" || (piece.kind == "open" && block) {39 for held in stack { containsBlocks = Set.insert(&containsBlocks, held) }40 }41 if piece.kind == "open" && block && !Set.contains(&VOID, piece.name) {42 stack = stack.push(index)43 } else if piece.kind == "close" && block {44 var depth = stack.length() - 145 while depth >= 0 && pieces[stack[depth]].name != piece.name { depth = depth - 1 }46 if depth >= 0 {47 closes = Map.insert(&closes, stack[depth], index)48 stack = stack.slice(0, depth)49 }50 }51 }52 var lines: Array[Str] = []53 var current: Array[Str] = []54 var level = 055 var index = 056 while index < pieces.length() {57 let piece = pieces[index]58 let block = Set.contains(&BLOCKS, piece.name) && (piece.kind == "open" || piece.kind == "close")59 if piece.kind == "doctype" || piece.kind == "comment" || piece.kind == "raw" || block {60 lines = flush(lines, ¤t, level)61 current = []62 }63 if piece.kind == "open" && block && !Set.contains(&VOID, piece.name) && Map.containsKey(&closes, index) && !Set.contains(&containsBlocks, index) {64 var whole: Array[Str] = []65 let end = Map.getOr(&closes, index, index)66 for position in index..(end + 1) { whole = whole.push(pieces[position].text) }67 lines = lines.push(indent(level) + whole.join("").trim())68 index = end + 169 continue70 }71 if piece.kind == "close" && block { level = if level > 0 { level - 1 } else { 0 } }72 if piece.kind == "doctype" || piece.kind == "comment" || piece.kind == "raw" || block {73 lines = lines.push(indent(level) + piece.text)74 if piece.kind == "open" && block && !Set.contains(&VOID, piece.name) && Map.containsKey(&closes, index) { level = level + 1 }75 } else { current = current.push(piece.text) }76 index = index + 177 }78 lines = flush(lines, ¤t, level)79 lines.join("\n") + "\n"80}81828384fn flush(lines: Array[Str], current: &Array[Str], level: Int) -> Array[Str] {85 var result = lines86 for line in current.join("").split("\n") {87 let text = line.trim()88 if !text.isEmpty() { result = result.push(indent(level) + text) }89 }90 result91}929394fn indent(level: Int) -> Str {95 var pieces: Array[Str] = []96 for _step in 0..level { pieces = pieces.push(INDENT) }97 pieces.join("")98}99100101fn split(document: Str) -> Array[Piece] {102 var pieces: Array[Piece] = []103 var rest = document104 while !rest.isEmpty() {105 let open = rest.indexOf("<")106 if open != 0 {107 let text = if open < 0 { rest } else { rest.take(open) }108 pieces = pieces.push(Piece{kind: "text", name: "", text: text})109 rest = rest.drop(text.length())110 continue111 }112 if rest.startsWith("<!--") {113 let end = rest.indexOf("-->")114 let length = if end < 0 { rest.length() } else { end + 3 }115 pieces = pieces.push(Piece{kind: "comment", name: "", text: rest.take(length)})116 rest = rest.drop(length)117 continue118 }119 let close = rest.indexOf(">")120 if close < 0 {121 pieces = pieces.push(Piece{kind: "text", name: "", text: rest})122 break123 }124 let tag = rest.take(close + 1)125 let closing = tag.startsWith("</")126 let body = if closing { tag.drop(2) } else { tag.drop(1) }127 var nameEnd = 0128 let cs = body.chars()129 while nameEnd < cs.length() && cs[nameEnd] != ' ' && cs[nameEnd] != '>' && cs[nameEnd] != '/' && cs[nameEnd] != '\n' { nameEnd = nameEnd + 1 }130 let name = body.take(nameEnd).toLower()131 if tag.startsWith("<!") || tag.startsWith("<?") {132 pieces = pieces.push(Piece{kind: "doctype", name: "", text: tag})133 rest = rest.drop(tag.length())134 } else if !closing && Set.contains(&VERBATIM, name) {135 let ending = "</" + name136 let found = rest.toLower().indexOf(ending)137 let end = if found < 0 { rest.length() } else { found + rest.drop(found).indexOf(">") + 1 }138 pieces = pieces.push(Piece{kind: "raw", name: name, text: rest.take(end)})139 rest = rest.drop(end)140 } else {141 pieces = pieces.push(Piece{kind: if closing { "close" } else { "open" }, name: name, text: tag})142 rest = rest.drop(tag.length())143 }144 }145 pieces146}147