
Sanitize.pudu
Pudu91 lines3.6 KB
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 Paths8910const 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 ])171819const 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 ])23242526export 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 == '>' { ">" } else if character == '"' { """ } 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("<")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}585960fn 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 "<" + Paths.escape(inner) + ">" }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}848586fn 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