///|
/// The decoded form of `s`: the escapes the specification lists; an
/// unknown or malformed escape is kept verbatim; `\` before a line break
/// (`\n` or `\r\n`; text blocks) adds nothing.
fn decode_escapes(s : String) -> String {
let sb = StringBuilder()
let n = s.length()
let mut i = 0
while i < n {
let c = s.code_unit_at(i).to_int().unsafe_to_char()
if c != '\\' || i + 1 >= n {
sb.write_char(c)
i += 1
continue
}
let e = s.code_unit_at(i + 1).to_int().unsafe_to_char()
match e {
'\\' | '"' | '/' => {
sb.write_char(e)
i += 2
}
'b' => {
sb.write_char('\u{8}')
i += 2
}
'f' => {
sb.write_char('\u{c}')
i += 2
}
'n' => {
sb.write_char('\n')
i += 2
}
'r' => {
sb.write_char('\r')
i += 2
}
't' => {
sb.write_char('\t')
i += 2
}
'\n' => i += 2
'\r' if i + 2 < n && s.code_unit_at(i + 2).to_int() == '\n'.to_int() =>
i += 3
'u' => {
let mut value = 0
let mut good = 0
while good < 4 && i + 2 + good < n {
let h = s.code_unit_at(i + 2 + good).to_int().unsafe_to_char()
guard is_hex(h) else { break }
let d = if is_digit(h) {
h.to_int() - '0'.to_int()
} else if h >= 'a' {
h.to_int() - 'a'.to_int() + 10
} else {
h.to_int() - 'A'.to_int() + 10
}
value = value * 16 + d
good += 1
}
if good == 4 {
sb.write_char(value.unsafe_to_char())
i += 6
} else {
sb.write_char('\\')
i += 1
}
}
_ => {
sb.write_char('\\')
i += 1
}
}
}
sb.to_string()
}
///|
/// The value of a `String` token: the lexeme with its quotes removed and
/// its escapes decoded.
pub fn unescape(lexeme : String) -> String {
let inner = if lexeme.length() >= 2 {
lexeme.unsafe_substring(start=1, end=lexeme.length() - 1)
} else {
""
}
decode_escapes(inner)
}
///|
fn leading_spaces(line : String) -> Int32 {
let mut k = 0
while k < line.length() && line.code_unit_at(k).to_int() == ' '.to_int() {
k += 1
}
k
}
///|
fn is_blank(line : String) -> Bool {
leading_spaces(line) == line.length()
}
///|
/// The lines of a text block body, line breaks removed (`\r\n` and `\n`).
fn block_lines(body : String) -> Array[String] {
body
.split(
(
#|
#|
),
)
.map(raw => raw.trim_end(chars="\r").to_owned())
.collect()
}
///|
/// The common leading-space count: over the non-blank lines, and over the
/// last line even when blank (it holds the closing quotes' indent).
fn common_indent(lines : Array[String]) -> Int32 {
let last = lines.length() - 1
let mut common = -1
for idx, line in lines {
if idx != last && is_blank(line) {
continue
}
let k = leading_spaces(line)
if common < 0 || k < common {
common = k
}
}
if common < 0 {
0
} else {
common
}
}
///|
/// The value of a `TextBlock` token, per the specification: drop the
/// opening quotes, optional spaces and the first line break; split the rest
/// into lines; remove the common indent and trailing spaces; join with
/// `\n`; then decode escapes.
pub fn text_block_value(lexeme : String) -> String {
let n = lexeme.length()
guard n >= 6 else { return "" }
let mut i = 3
while i < n &&
(
lexeme.code_unit_at(i).to_int() == ' '.to_int() ||
lexeme.code_unit_at(i).to_int() == '\t'.to_int()
) {
i += 1
}
if i < n && lexeme.code_unit_at(i).to_int() == '\r'.to_int() {
i += 1
}
if i < n && lexeme.code_unit_at(i).to_int() == '\n'.to_int() {
i += 1
}
let lines = block_lines(lexeme.unsafe_substring(start=i, end=n - 3))
let common = common_indent(lines)
let last = lines.length() - 1
lines
.mapi((idx, line) => {
if idx == last && is_blank(line) {
// The closing quotes stood on their own line: the value ends with `\n`.
""
} else {
let cut = if leading_spaces(line) < common {
leading_spaces(line)
} else {
common
}
line
.unsafe_substring(start=cut, end=line.length())
.trim_end(chars=" ")
.to_owned()
}
})
.join(
(
#|
#|
),
)
|> decode_escapes
}