///|
/// The mark a held-back stretch leaves in the text around it: private-use
/// characters, which no Markdown reader makes anything of, with the number
/// of the piece held back between them.
let slot_open : String = "\u{E000}"
///|
let slot_close : String = "\u{E001}"
///|
fn slot_mark(index : Int32) -> String {
slot_open + index.to_string() + slot_close
}
///|
/// Render a stretch of one language. What it writes goes through that
/// language's conversion, `Lang.finish`; when the stretch around it converts
/// its own text too, the result is held back under a mark, so the outer
/// conversion cannot touch it, and the marks are replaced once it is done.
fn Run::run_stretch(
self : Run,
reading : Reading,
body : Array[@syntax.Node],
) -> Unit raise @spec.Refused {
let finish = reading.lang.finish
if finish is None && self.slots.is_empty() {
self.run_nodes(body)
return
}
let (text, held) = self.capture_slots(reading, () => self.run_nodes(body))
let converted = match finish {
Some(f) => f(text, reading)
None => text
}
let result = if held.is_empty() {
converted
} else {
restore(converted, held)
}
if self.slots.is_empty() {
self.write(result)
} else {
let outer = self.slots[self.slots.length() - 1]
let index = outer.length()
outer.push(result)
let block = self.at_line_start
if block {
self.write("\n")
}
self.write(slot_mark(index))
if block {
self.write("\n\n")
}
}
}
///|
/// Run `body` in the reading `reading` with output going to a fresh buffer
/// and a slot table of its own, and return what it wrote with what nested
/// stretches held back.
fn Run::capture_slots(
self : Run,
reading : Reading,
body : () -> Unit raise @spec.Refused,
) -> (String, Array[String]) raise @spec.Refused {
let outer_out = self.out
let written = self.written
let extended = self.extended
let at_line_start = self.at_line_start
let current = self.current
let held : Array[String] = []
self.out = StringBuilder()
self.written = 0
self.extended = false
self.at_line_start = true
self.current = Some(reading)
self.slots.push(held)
defer {
ignore(self.slots.pop())
self.out = outer_out
self.written = written
self.extended = extended
self.at_line_start = at_line_start
self.current = current
}
body()
(self.out.to_string(), held)
}
///|
/// The number a mark carries, or `None` when the marks do not hold one.
fn slot_number(digits : StringView) -> Int32? {
if digits.is_empty() || digits.length() > 9 {
return None
}
let mut n = 0
for i in 0.. '9' {
return None
}
n = n * 10 + (c.to_int() - '0'.to_int())
}
Some(n)
}
///|
/// Put the held-back pieces back where their marks stand. A mark that came
/// out as a paragraph of its own takes the paragraph with it: a block of HTML
/// belongs between the blocks around it, not inside a ``.
fn restore(text : String, held : Array[String]) -> String {
let out = StringBuilder()
let mut cursor = 0
let mut base = 0
let mut rest : StringView = text[:]
while rest.find(slot_open) is Some(at) {
let head = base + at
let tail = rest[at + slot_open.length():]
guard tail.find(slot_close) is Some(width) else { break }
let end = head + slot_open.length() + width + slot_close.length()
match slot_number(tail[:width]) {
Some(index) if index >= 0 && index < held.length() => {
let mut from = head
let mut to = end
if from >= 3 &&
text[from - 3:from] == "
" &&
to + 4 <= text.length() &&
text[to:to + 4] == "
" {
from -= 3
to += 4
// The paragraph's own newline and the block's are the same one.
if to + 1 <= text.length() &&
text[to:to + 1] == "\n" &&
held[index].has_suffix("\n") {
to += 1
}
}
out.write_view(text[cursor:from])
out.write_string(held[index])
cursor = to
}
_ => ()
}
rest = rest[end - base:]
base = end
}
out.write_view(text[cursor:])
out.to_string()
}