
Inline.pudu
Pudu479 lines20.4 KB
1/** @Docgen.Inline.Module — phrasing syntax parsed into article inline nodes */2module PuduLangDocgen.Inline34import Std.Char as Char5import Std.Map as Map6import PuduLangDocgen.Markdown.Crossref as Crossref7import PuduLangDocgen.Markdown.Lexicon as Lexicon8import PuduLangDocgen.Markdown.Syntax as Syntax91011const PUNCTUATION: Str = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`\{|\}~"1213/** @Docgen.Inline.Scan — characters of one block with line offsets and link definitions */14type Scan = { cs: Array[Char], breaks: Array[Int], definitions: Map[Str, Syntax.Target], line: Int }151617export fn parse(text: Str, definitions: &Map[Str, Syntax.Target], line: Int) -> Array[Syntax.Inline] {18 let cs = text.chars()19 var breaks: Array[Int] = []20 var count = 021 for character in cs {22 breaks = breaks.push(count)23 if character == '\n' { count = count + 1 }24 }25 let scan = Scan{cs: cs, breaks: breaks.push(count), definitions: *definitions, line: line}26 span(&scan, 0, cs.length(), 0)27}282930export fn label(text: Str) -> Str {31 text.trim().toLower().split("\n").join(" ").split("\t").join(" ").split(" ").filter(|part: Str| !part.isEmpty()).join(" ")32}333435export fn plain(inlines: &Array[Syntax.Inline]) -> Str {36 var pieces: Array[Str] = []37 for node in *inlines {38 let piece = match node {39 case Syntax.Text(written) => written40 case Syntax.Code(written) => written41 case Syntax.Emphasis(children) => plain(&children)42 case Syntax.Strong(children) => plain(&children)43 case Syntax.Strike(children) => plain(&children)44 case Syntax.Subscript(children) => plain(&children)45 case Syntax.Superscript(children) => plain(&children)46 case Syntax.Inserted(children) => plain(&children)47 case Syntax.Marked(children) => plain(&children)48 case Syntax.Note(_label) => ""49 case Syntax.Anchor(_target, children) => plain(&children)50 case Syntax.Picture(_target, alt) => alt51 case Syntax.Xref(reference) => if reference.text.isEmpty() { reference.uid } else { plain(&reference.text) }52 case Syntax.Math(written) => written53 case Syntax.Markup(_written) => ""54 case Syntax.LineBreak => " "55 case Syntax.SoftBreak => " "56 case Syntax.Fragment(_path, children) => plain(&children)57 }58 pieces = pieces.push(piece)59 }60 pieces.join("")61}626364fn at(scan: &Scan, index: Int) -> Char {65 if index < 0 || index >= scan.cs.length() { ' ' } else { scan.cs[index] }66}676869fn slice(scan: &Scan, from: Int, to: Int) -> Str { Lexicon.slice(&scan.cs, from, to) }707172fn lineAt(scan: &Scan, index: Int) -> Int { scan.line + scan.breaks[index] }737475fn blank(character: Char) -> Bool { Lexicon.blank(character) }767778fn punctuation(character: Char) -> Bool { PUNCTUATION.contains(character.toText()) }798081fn word(character: Char) -> Bool { Char.isAlphanumeric(character) || character.code() > 127 }828384fn flushed(nodes: Array[Syntax.Inline], pending: &Array[Str]) -> Array[Syntax.Inline] {85 if pending.isEmpty() { nodes } else { nodes.push(Syntax.Text(pending.join(""))) }86}878889fn span(scan: &Scan, from: Int, to: Int, depth: Int) -> Array[Syntax.Inline] {90 var nodes: Array[Syntax.Inline] = []91 var pending: Array[Str] = []92 var index = from93 while index < to {94 let character = scan.cs[index]95 let found = if depth > 32 { None } else { construct(scan, index, to, depth) }96 match found {97 case Some((made, next)) => {98 nodes = flushed(nodes, &pending)99 pending = []100 nodes = nodes.concat(made)101 index = next102 }103 case None => {104 if character == '\\' && index + 1 < to && punctuation(scan.cs[index + 1]) {105 pending = pending.push(scan.cs[index + 1].toText())106 index = index + 2107 } else if character == '\\' && index + 1 < to && scan.cs[index + 1] == '\n' {108 nodes = flushed(nodes, &pending).push(Syntax.LineBreak)109 pending = []110 index = index + 2111 } else if character == '\n' {112 var trailing = 0113 while pending.length() > trailing && pending[pending.length() - 1 - trailing] == " " { trailing = trailing + 1 }114 pending = pending.slice(0, pending.length() - trailing)115 nodes = flushed(nodes, &pending).push(if trailing >= 2 { Syntax.LineBreak } else { Syntax.SoftBreak })116 pending = []117 index = index + 1118 while index < to && (scan.cs[index] == ' ' || scan.cs[index] == '\t') { index = index + 1 }119 } else if character == '*' || character == '_' || character == '~' || character == '`' {120 let run = runLength(scan, index, to, character)121 pending = pending.push(slice(scan, index, index + run))122 index = index + run123 } else {124 pending = pending.push(character.toText())125 index = index + 1126 }127 }128 }129 }130 flushed(nodes, &pending)131}132133134fn construct(scan: &Scan, index: Int, to: Int, depth: Int) -> Option[(Array[Syntax.Inline], Int)] {135 let character = scan.cs[index]136 if character == '`' { return code(scan, index, to) }137 if character == '*' || character == '_' || character == '~' { return emphasis(scan, index, to, depth) }138 if character == '^' { return paired(scan, index, to, depth, 1) }139 if character == '+' || character == '=' { return paired(scan, index, to, depth, 2) }140 if character == '[' { return bracket(scan, index, to, depth) }141 if character == '!' && at(scan, index + 1) == '[' { return image(scan, index, to) }142 if character == '<' { return angle(scan, index, to) }143 if character == '@' { return mention(scan, index, to) }144 if character == '&' { return entity(scan, index, to) }145 if character == ':' { return emoji(scan, index, to) }146 if character == '$' { return math(scan, index, to) }147 if character == 'h' || character == 'w' { return bare(scan, index, to) }148 None149}150151152fn runLength(scan: &Scan, index: Int, to: Int, character: Char) -> Int {153 var end = index154 while end < to && scan.cs[end] == character { end = end + 1 }155 end - index156}157158159fn code(scan: &Scan, index: Int, to: Int) -> Option[(Array[Syntax.Inline], Int)] {160 let run = runLength(scan, index, to, '`')161 let close = codeEnd(scan, index, to)162 if close < 0 { return None }163 var content = slice(scan, index + run, close).replace("\n", " ")164 if content.length() >= 2 && content.startsWith(" ") && content.endsWith(" ") && !content.trim().isEmpty() {165 content = content.drop(1).take(content.length() - 2)166 }167 Some(([Syntax.Code(content)], close + run))168}169170171fn codeEnd(scan: &Scan, index: Int, to: Int) -> Int {172 let run = runLength(scan, index, to, '`')173 var cursor = index + run174 while cursor < to {175 if scan.cs[cursor] == '`' {176 let found = runLength(scan, cursor, to, '`')177 if found == run { return cursor }178 cursor = cursor + found179 } else { cursor = cursor + 1 }180 }181 -1182}183184185fn opens(scan: &Scan, index: Int, run: Int, to: Int) -> Bool {186 let before = at(scan, index - 1)187 let after = if index + run >= to { ' ' } else { scan.cs[index + run] }188 let left = !blank(after) && (!punctuation(after) || blank(before) || punctuation(before))189 if scan.cs[index] == '_' { left && !word(before) } else { left }190}191192193fn closes(scan: &Scan, index: Int, run: Int, to: Int) -> Bool {194 let before = at(scan, index - 1)195 let after = if index + run >= to { ' ' } else { scan.cs[index + run] }196 let right = !blank(before) && (!punctuation(before) || blank(after) || punctuation(after))197 if scan.cs[index] == '_' { right && !word(after) } else { right }198}199200201fn closing(scan: &Scan, from: Int, to: Int, delimiter: Char, wanted: Int, depth: Int) -> Int {202 var cursor = from203 while cursor < to {204 let character = scan.cs[cursor]205 if character == '\\' {206 cursor = cursor + 2207 } else if character == '`' {208 let end = codeEnd(scan, cursor, to)209 cursor = if end < 0 { cursor + runLength(scan, cursor, to, '`') } else { end + runLength(scan, cursor, to, '`') }210 } else if character == '*' || character == '_' || character == '~' {211 let run = runLength(scan, cursor, to, character)212 let canClose = closes(scan, cursor, run, to)213 let canOpen = opens(scan, cursor, run, to)214 if character == delimiter && canClose && (!canOpen || run == wanted) { return cursor }215 if canOpen && depth < 32 {216 let nested = closing(scan, cursor + run, to, character, run, depth + 1)217 cursor = if nested < 0 { cursor + run } else { nested + runLength(scan, nested, to, character) }218 } else { cursor = cursor + run }219 } else { cursor = cursor + 1 }220 }221 -1222}223224225fn emphasis(scan: &Scan, index: Int, to: Int, depth: Int) -> Option[(Array[Syntax.Inline], Int)] {226 let delimiter = scan.cs[index]227 let run = runLength(scan, index, to, delimiter)228 if delimiter == '~' && run > 2 { return None }229 if !opens(scan, index, run, to) { return None }230 let close = closing(scan, index + run, to, delimiter, run, 0)231 if close < 0 { return None }232 let closingRun = runLength(scan, close, to, delimiter)233 var used = if run < closingRun { run } else { closingRun }234 if used > 3 { used = 3 }235 if delimiter == '~' && closingRun != run { return None }236 let start = index + run237 let inner = span(scan, start, close, depth + 1)238 let node = if delimiter == '~' && used == 1 { Syntax.Subscript(inner) }239 else if delimiter == '~' { Syntax.Strike(inner) }240 else if used == 1 { Syntax.Emphasis(inner) }241 else if used == 2 { Syntax.Strong(inner) }242 else { Syntax.Emphasis([Syntax.Strong(inner)]) }243 let extra = run - used244 let lead: Array[Syntax.Inline] = if extra > 0 { [Syntax.Text(slice(scan, index, index + extra))] } else { [] }245 Some((lead.push(node), close + used))246}247248249250fn paired(scan: &Scan, index: Int, to: Int, depth: Int, length: Int) -> Option[(Array[Syntax.Inline], Int)] {251 let delimiter = scan.cs[index]252 if runLength(scan, index, to, delimiter) != length || index + length >= to || blank(scan.cs[index + length]) { return None }253 var cursor = index + length254 while cursor < to {255 if scan.cs[cursor] == '\\' {256 cursor = cursor + 2257 continue258 }259 if scan.cs[cursor] == delimiter {260 let run = runLength(scan, cursor, to, delimiter)261 if run == length && !blank(scan.cs[cursor - 1]) {262 let inner = span(scan, index + length, cursor, depth + 1)263 let node = if delimiter == '^' { Syntax.Superscript(inner) } else if delimiter == '+' { Syntax.Inserted(inner) } else { Syntax.Marked(inner) }264 return Some(([node], cursor + length))265 }266 cursor = cursor + run267 } else { cursor = cursor + 1 }268 }269 None270}271272273fn bracketEnd(scan: &Scan, index: Int, to: Int) -> Int {274 var level = 0275 var cursor = index276 while cursor < to {277 let character = scan.cs[cursor]278 if character == '\\' { cursor = cursor + 2279 continue }280 if character == '`' {281 let end = codeEnd(scan, cursor, to)282 if end >= 0 {283 cursor = end + runLength(scan, cursor, to, '`')284 continue285 }286 }287 if character == '[' { level = level + 1 }288 if character == ']' {289 level = level - 1290 if level == 0 { return cursor }291 }292 cursor = cursor + 1293 }294 -1295}296297298fn destination(scan: &Scan, index: Int, to: Int) -> Option[(Syntax.Target, Int)] {299 if at(scan, index) != '(' || index >= to { return None }300 var cursor = index + 1301 while cursor < to && blank(scan.cs[cursor]) { cursor = cursor + 1 }302 let begin = cursor303 var written = ""304 if at(scan, cursor) == '<' {305 cursor = cursor + 1306 while cursor < to && scan.cs[cursor] != '>' && scan.cs[cursor] != '\n' { cursor = cursor + 1 }307 if cursor >= to || scan.cs[cursor] != '>' { return None }308 written = slice(scan, begin + 1, cursor)309 cursor = cursor + 1310 } else {311 var level = 0312 while cursor < to && !blank(scan.cs[cursor]) {313 let character = scan.cs[cursor]314 if character == '\\' && cursor + 1 < to { cursor = cursor + 1 }315 else if character == '(' { level = level + 1 }316 else if character == ')' {317 if level == 0 { break }318 level = level - 1319 }320 cursor = cursor + 1321 }322 written = slice(scan, begin, cursor)323 }324 while cursor < to && blank(scan.cs[cursor]) { cursor = cursor + 1 }325 var title = ""326 let opener = at(scan, cursor)327 if cursor < to && (opener == '"' || opener == '\'' || opener == '(') {328 let closer = if opener == '(' { ')' } else { opener }329 let titleStart = cursor + 1330 cursor = titleStart331 while cursor < to && scan.cs[cursor] != closer { cursor = cursor + 1 }332 if cursor >= to { return None }333 title = slice(scan, titleStart, cursor)334 cursor = cursor + 1335 while cursor < to && blank(scan.cs[cursor]) { cursor = cursor + 1 }336 }337 if cursor >= to || scan.cs[cursor] != ')' { return None }338 Some((Syntax.Target{destination: written, title: title, line: lineAt(scan, index)}, cursor + 1))339}340341342fn referenced(scan: &Scan, text: Str, after: Int, to: Int) -> Option[(Syntax.Target, Int)] {343 if at(scan, after) == '[' && after < to {344 let close = bracketEnd(scan, after, to)345 if close < 0 { return None }346 let written = slice(scan, after + 1, close)347 let key = if written.trim().isEmpty() { label(text) } else { label(written) }348 let found = Map.get(&scan.definitions, key) ?349 return Some((found, close + 1))350 }351 let found = Map.get(&scan.definitions, label(text)) ?352 Some((found, after))353}354355356fn bracket(scan: &Scan, index: Int, to: Int, depth: Int) -> Option[(Array[Syntax.Inline], Int)] {357 if at(scan, index + 1) == '!' && slice(scan, index + 2, if index + 9 <= to { index + 9 } else { to }).toLower() == "include" {358 return include(scan, index, to)359 }360 let close = bracketEnd(scan, index, to)361 if close < 0 { return None }362 let text = slice(scan, index + 1, close)363 if text.startsWith("^") && text.length() > 1 && !text.contains(" ") && at(scan, close + 1) != '(' && at(scan, close + 1) != ':' {364 return Some(([Syntax.Note(text.drop(1))], close + 1))365 }366 let found = match destination(scan, close + 1, to) {367 case Some(held) => Some(held)368 case None => referenced(scan, text, close + 1, to)369 }370 let (target, next) = found ?371 let children = span(scan, index + 1, close, depth + 1)372 if target.destination.startsWith("xref:") {373 return Some(([Crossref.span(target.destination.drop(5), children, target.line, false)], next))374 }375 Some(([Syntax.Anchor(target, children)], next))376}377378379fn include(scan: &Scan, index: Int, to: Int) -> Option[(Array[Syntax.Inline], Int)] {380 var cursor = index + 9381 while cursor < to && scan.cs[cursor] == ' ' { cursor = cursor + 1 }382 if at(scan, cursor) != '[' { return None }383 let close = bracketEnd(scan, cursor, to)384 if close < 0 { return None }385 let (target, next) = destination(scan, close + 1, to) ?386 if at(scan, next) != ']' || next >= to { return None }387 Some(([Syntax.Fragment(target.destination, [Syntax.Text(slice(scan, cursor + 1, close))])], next + 1))388}389390391fn image(scan: &Scan, index: Int, to: Int) -> Option[(Array[Syntax.Inline], Int)] {392 let close = bracketEnd(scan, index + 1, to)393 if close < 0 { return None }394 let text = slice(scan, index + 2, close)395 let found = match destination(scan, close + 1, to) {396 case Some(held) => Some(held)397 case None => referenced(scan, text, close + 1, to)398 }399 let (target, next) = found ?400 let alt = plain(&span(scan, index + 2, close, 1))401 Some(([Syntax.Picture(target, alt)], next))402}403404405fn angle(scan: &Scan, index: Int, to: Int) -> Option[(Array[Syntax.Inline], Int)] {406 var close = index + 1407 while close < to && scan.cs[close] != '>' && scan.cs[close] != '<' { close = close + 1 }408 if at(scan, index + 1) == '!' && slice(scan, index, if index + 4 <= to { index + 4 } else { to }) == "<!--" {409 var end = index + 4410 while end + 2 < to && !(scan.cs[end] == '-' && scan.cs[end + 1] == '-' && scan.cs[end + 2] == '>') { end = end + 1 }411 if end + 2 >= to { return None }412 return Some(([], end + 3))413 }414 if close >= to || scan.cs[close] != '>' { return None }415 let inner = slice(scan, index + 1, close)416 let line = lineAt(scan, index)417 if inner.startsWith("xref:") && !inner.contains(" ") { return Some(([Crossref.span(inner.drop(5), [], line, false)], close + 1)) }418 if inner.startsWith("xref ") {419 let found = Crossref.element(inner, line) ?420 return Some(([found], close + 1))421 }422 match Lexicon.autolink(inner) {423 case Some(target) => { return Some(([Syntax.Anchor(Syntax.Target{destination: target, title: "", line: line}, [Syntax.Text(inner)])], close + 1)) }424 case None => {}425 }426 let markup = Lexicon.tag(inner) ?427 Some(([Syntax.Markup(markup)], close + 1))428}429430431fn mention(scan: &Scan, index: Int, to: Int) -> Option[(Array[Syntax.Inline], Int)] {432 let before = at(scan, index - 1)433 if index > 0 && !blank(before) && before != '(' && before != '[' { return None }434 let line = lineAt(scan, index)435 let quote = at(scan, index + 1)436 if index + 1 < to && (quote == '"' || quote == '\'') {437 var end = index + 2438 while end < to && scan.cs[end] != quote { end = end + 1 }439 if end >= to || end == index + 2 { return None }440 return Some(([Crossref.span(slice(scan, index + 2, end), [], line, true)], end + 1))441 }442 if index + 1 >= to || !Char.isLetter(scan.cs[index + 1]) { return None }443 var end = index + 1444 while end < to && (word(scan.cs[end]) || ".-_#*()`<>,".contains(scan.cs[end].toText())) { end = end + 1 }445 while end > index + 1 && ".,;:)>".contains(scan.cs[end - 1].toText()) { end = end - 1 }446 Some(([Crossref.span(slice(scan, index + 1, end), [], line, true)], end))447}448449450fn entity(scan: &Scan, index: Int, to: Int) -> Option[(Array[Syntax.Inline], Int)] {451 let (decoded, next) = Lexicon.entity(&scan.cs, index, to) ?452 Some(([Syntax.Text(decoded)], next))453}454455456fn emoji(scan: &Scan, index: Int, to: Int) -> Option[(Array[Syntax.Inline], Int)] {457 let (picture, next) = Lexicon.emoji(&scan.cs, index, to) ?458 Some(([Syntax.Text(picture)], next))459}460461462fn math(scan: &Scan, index: Int, to: Int) -> Option[(Array[Syntax.Inline], Int)] {463 if index + 1 >= to || blank(scan.cs[index + 1]) || scan.cs[index + 1] == '$' { return None }464 var end = index + 1465 while end < to && scan.cs[end] != '$' && scan.cs[end] != '\n' {466 if scan.cs[end] == '\\' { end = end + 1 }467 end = end + 1468 }469 if end >= to || scan.cs[end] != '$' || blank(scan.cs[end - 1]) || Char.isDigit(at(scan, end + 1)) { return None }470 Some(([Syntax.Math(slice(scan, index + 1, end))], end + 1))471}472473474fn bare(scan: &Scan, index: Int, to: Int) -> Option[(Array[Syntax.Inline], Int)] {475 let (written, next) = Lexicon.address(&scan.cs, index, to) ?476 let target = if written.toLower().startsWith("www.") { "https://" + written } else { written }477 Some(([Syntax.Anchor(Syntax.Target{destination: target, title: "", line: lineAt(scan, index)}, [Syntax.Text(written)])], next))478}479