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

Sanitize.pudu

Pudu91 lines3.6 KB

GitHub ↗
1/** @Docgen.Markdown.Sanitize — raw HTML reduced to inert allowed elements */2module PuduLangDocgen.Markdown.Sanitize34import Std.Char as Char5import Std.Set as Set6import PuduLangDocgen.Markdown.Directives as Directives7import PuduLangDocgen.Paths as Paths89/// Elements kept from raw HTML; any other tag is shown as text.10const ELEMENTS: Set[Str] = setOf([11    "a", "abbr", "article", "aside", "b", "blockquote", "br", "caption", "center", "cite", "code", "col",12    "colgroup", "dd", "del", "details", "dfn", "div", "dl", "dt", "em", "figcaption", "figure", "footer",13    "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr", "i", "img", "ins", "kbd", "li", "mark", "nav", "ol",14    "p", "pre", "q", "s", "samp", "section", "small", "span", "strong", "sub", "summary", "sup", "table",15    "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul", "var", "wbr"16  ])1718/// Attributes kept on allowed elements; `href` and `src` must also be safe destinations.19const ATTRIBUTES: Set[Str] = setOf([20    "class", "id", "title", "align", "colspan", "rowspan", "open", "alt", "width", "height", "src", "href",21    "lang", "dir", "start", "type", "scope", "aria-label", "aria-hidden", "role", "name", "target"22  ])2324/// Raw HTML with allowed elements rebuilt from their allowed attributes.25/// Other tags, scripts, styles, and event handlers are escaped into visible text.26export fn clean(html: Str) -> Str {27  let cs = html.chars()28  var pieces: Array[Str] = []29  var index = 030  while index < cs.length() {31    let character = cs[index]32    if character != '<' {33      pieces = pieces.push(if character == '>' { "&gt;" } else if character == '"' { "&quot;" } else { character.toText() })34      index = index + 135      continue36    }37    var close = index + 138    var quote = ' '39    while close < cs.length() && (cs[close] != '>' || quote != ' ') {40      if (cs[close] == '"' || cs[close] == '\'') && quote == ' ' { quote = cs[close] } else if cs[close] == quote { quote = ' ' }41      close = close + 142    }43    if close >= cs.length() {44      pieces = pieces.push("&lt;")45      index = index + 146      continue47    }48    let inner = slice(&cs, index + 1, close)49    if inner.startsWith("!--") && inner.endsWith("--") {50      index = close + 151      continue52    }53    pieces = pieces.push(element(inner))54    index = close + 155  }56  pieces.join("")57}5859/// One tag rebuilt, or escaped when it is not allowed.60fn element(inner: Str) -> Str {61  let closing = inner.startsWith("/")62  let body = if closing { inner.drop(1) } else { inner }63  let cs = body.chars()64  var end = 065  while end < cs.length() && (Char.isAlphanumeric(cs[end]) || cs[end] == '-') { end = end + 1 }66  let name = body.take(end).toLower()67  if name.isEmpty() || !Set.contains(&ELEMENTS, name) { return "&lt;" + Paths.escape(inner) + "&gt;" }68  if closing { return "</" + name + ">" }69  var kept: Array[Str] = []70  var external = false71  let rest = body.drop(end)72  let selfClosing = rest.trim().endsWith("/")73  for (key, value) in Directives.attributes(if selfClosing { rest.trim().take(rest.trim().length() - 1) } else { rest }) {74    let lowered = key.toLower()75    if Set.contains(&ATTRIBUTES, lowered) {76      let safe = if lowered == "href" || lowered == "src" { Paths.href(value) } else { true }77      if safe { kept = kept.push(" " + lowered + "=\"" + Paths.escape(value) + "\"") }78      if lowered == "target" { external = true }79    }80  }81  if external { kept = kept.push(" rel=\"noopener noreferrer\"") }82  "<" + name + kept.join("") + ">"83}8485/// Text of the characters in a half-open range.86fn slice(cs: &Array[Char], from: Int, to: Int) -> Str {87  var pieces: Array[Str] = []88  for index in from..to { pieces = pieces.push(cs[index].toText()) }89  pieces.join("")90}91