Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-docgen

Documentation publishing for Pudu: articles, API references, navigation, search, and static websites

0.1.0Apache-2.01

InstallClose

Format.pudu

Pudu147 lines6.0 KB

GitHub ↗
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 }910/// Elements laid out on lines of their own.11const 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  ])1819/// Elements whose content is copied exactly.20const VERBATIM: Set[Str] = setOf(["pre", "script", "style", "textarea"])2122/// Elements that never have content.23const VOID: Set[Str] = setOf(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"])2425/// Indentation added per nesting level.26const INDENT: Str = "  "2728/// HTML with block elements on their own lines indented two spaces per level. Text, inline29/// markup, and the content of `pre`, `script`, `style`, and `textarea` keep their exact form.30export 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, &current, 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, &current, level)79  lines.join("\n") + "\n"80}8182/// Lines with pending inline content added, each source line indented to the level; line83/// breaks between inline content carry no meaning outside verbatim elements.84fn 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}9293/// Indentation for a nesting level.94fn indent(level: Int) -> Str {95  var pieces: Array[Str] = []96  for _step in 0..level { pieces = pieces.push(INDENT) }97  pieces.join("")98}99100/// A document split into tags, text, comments, and verbatim elements.101fn 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