
Lexicon.pudu
Pudu130 lines5.6 KB
1/** @Docgen.Markdown.Lexicon — character references, short codes, tags, and web addresses */2module PuduLangDocgen.Markdown.Lexicon34import Std.Char as Char5import Std.Set as Set6import PuduLangDocgen.Markdown.Emoji as Emoji7import PuduLangDocgen.Markdown.Entities as Entities8910const TAGS: Set[Str] = setOf([11 "abbr", "b", "br", "cite", "code", "del", "dfn", "em", "i", "ins", "kbd", "mark", "q", "s", "samp",12 "small", "strong", "sub", "sup", "time", "u", "var", "wbr"13 ])141516const NAME_LIMIT: Int = 32171819const CODE_LIMIT: Int = 96202122export fn slice(cs: &Array[Char], from: Int, to: Int) -> Str {23 var pieces: Array[Str] = []24 for index in from..to { pieces = pieces.push(cs[index].toText()) }25 pieces.join("")26}272829export fn blank(character: Char) -> Bool { Char.isWhitespace(character) || character == '\u{a0}' }303132export fn entity(cs: &Array[Char], index: Int, to: Int) -> Option[(Str, Int)] {33 var end = index + 134 while end < to && end - index <= NAME_LIMIT && cs[end] != ';' && (Char.isAlphanumeric(cs[end]) || cs[end] == '#') { end = end + 1 }35 if end >= to || cs[end] != ';' || end == index + 1 { return None }36 let name = slice(cs, index + 1, end)37 if !name.startsWith("#") {38 let decoded = Entities.lookup(name) ?39 return Some((decoded, end + 1))40 }41 let hex = name.startsWith("#x") || name.startsWith("#X")42 let digits = if hex { name.drop(2) } else { name.drop(1) }43 if digits.isEmpty() || digits.length() > 7 { return None }44 var value = 045 for digit in digits.chars() {46 let amount = if hex { Char.toHexDigit(digit) ? } else { Char.toDigit(digit) ? }47 value = value * (if hex { 16 } else { 10 }) + amount48 }49 if value == 0 || value > 1114111 { return Some(("\u{fffd}", end + 1)) }50 match Char.fromCode(value) {51 case Some(decoded) => Some((decoded.toText(), end + 1))52 case None => Some(("\u{fffd}", end + 1))53 }54}555657export fn emoji(cs: &Array[Char], index: Int, to: Int) -> Option[(Str, Int)] {58 var end = index + 159 while end < to && end - index <= CODE_LIMIT && (Char.isAlphanumeric(cs[end]) || "_+-".contains(cs[end].toText())) { end = end + 1 }60 if end >= to || cs[end] != ':' { return None }61 let picture = Emoji.lookup(slice(cs, index + 1, end)) ?62 Some((picture, end + 1))63}64656667export fn address(cs: &Array[Char], index: Int, to: Int) -> Option[(Str, Int)] {68 let before = if index > 0 { cs[index - 1] } else { ' ' }69 if index > 0 && !blank(before) && !"(*_~".contains(before.toText()) { return None }70 let head = slice(cs, index, if index + 8 <= to { index + 8 } else { to }).toLower()71 let web = head.startsWith("www.")72 if !head.startsWith("https://") && !head.startsWith("http://") && !web { return None }73 var end = index74 while end < to && !blank(cs[end]) && cs[end] != '<' { end = end + 1 }75 var opened = 076 for position in index..end {77 if cs[position] == '(' { opened = opened + 1 }78 if cs[position] == ')' { opened = opened - 1 }79 }80 var trimming = true81 while trimming && end > index {82 let last = cs[end - 1]83 if ".,:;!?\"'*_~".contains(last.toText()) { end = end - 1 } else if last == ')' && opened < 0 {84 opened = opened + 185 end = end - 186 } else { trimming = false }87 }88 let written = slice(cs, index, end)89 let minimum = if web { 5 } else { written.indexOf("//") + 3 }90 if written.length() < minimum { return None }91 Some((written, end))92}939495export fn autolink(inner: Str) -> Option[Str] {96 if inner.contains(" ") || inner.contains("\n") || inner.isEmpty() { return None }97 let lowered = inner.toLower()98 if lowered.startsWith("https://") || lowered.startsWith("http://") || lowered.startsWith("mailto:") { return Some(inner) }99 let sign = inner.indexOf("@")100 if sign > 0 && inner.drop(sign + 1).contains(".") && !inner.contains(":") && !inner.contains("/") { return Some("mailto:" + inner) }101 None102}103104105export fn tag(inner: Str) -> Option[Str] {106 let closing = inner.startsWith("/")107 let body = if closing { inner.drop(1) } else { inner }108 let name = body.split(" ")[0].split("/")[0].split("\n")[0].toLower()109 if !Set.contains(&TAGS, name) { return None }110 if closing { return Some("</" + name + ">") }111 if name == "br" || name == "wbr" { return Some("<" + name + ">") }112 let title = attribute(body, "title")113 if title.isEmpty() { Some("<" + name + ">") } else { Some("<" + name + " title=\"" + escaped(title) + "\">") }114}115116117export fn attribute(element: Str, name: Str) -> Str {118 let marker = name + "=\""119 var start = element.indexOf(" " + marker)120 if start < 0 && element.startsWith(marker) { start = -1 } else if start < 0 { return "" }121 let rest = element.drop(start + 1 + marker.length())122 let end = rest.indexOf("\"")123 if end < 0 { "" } else { rest.take(end) }124}125126127fn escaped(text: Str) -> Str {128 text.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)129}130