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

Lexicon.pudu

Pudu130 lines5.6 KB

GitHub ↗
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 Entities89/// Inline HTML elements kept as markup; every other tag is shown as text.10const 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  ])1415/// Longest name a character reference may have.16const NAME_LIMIT: Int = 321718/// Longest emoji short code.19const CODE_LIMIT: Int = 962021/// Text of the characters in a half-open range.22export 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}2728/// Whether a character counts as white space between words.29export fn blank(character: Char) -> Bool { Char.isWhitespace(character) || character == '\u{a0}' }3031/// A decoded character reference at an index and the index after it.32export 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}5556/// The emoji a `:name:` short code at an index stands for, and the index after it.57export 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}6465/// A bare web address starting with `http://`, `https://`, or `www.`, and the index after it.66/// Trailing punctuation and unbalanced closing parentheses stay outside the address.67export 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}9394/// Whether text inside angle brackets is an autolink, answering its destination.95export 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}103104/// Allowed inline HTML rebuilt without attributes other than `title`, or none for other tags.105export 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}115116/// The double-quoted value of a named attribute inside a tag, or the empty text.117export 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}125126/// Text escaped for a quoted attribute.127fn escaped(text: Str) -> Str {128  text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")129}130