//! Scanners for fragments of CommonMark syntax.
//!
//! Byte-oriented scanning over a UTF-8 `Bytes` view.
///|
fn Byte::is_ascii_whitespace_no_nl(c : Byte) -> Bool {
c.to_char().is_ascii_whitespace() && c != b'\n' && c != b'\r'
}
///|
fn Byte::is_ascii_alphanumeric(c : Byte) -> Bool {
let ch = c.to_char()
ch.is_ascii_alphabetic() || ch.is_ascii_digit()
}
///|
fn Byte::is_ascii_letterdigitdash(c : Byte) -> Bool {
c == b'-' || c.is_ascii_alphanumeric()
}
///|
fn Byte::is_valid_unquoted_attr_value_char(c : Byte) -> Bool {
let b = c.to_int()
b != 0x27 &&
b != 0x22 &&
b != 0x20 &&
b != 0x3d &&
b != 0x3e &&
b != 0x3c &&
b != 0x60 &&
b != 0x0a &&
b != 0x0d
}
///|
/// scan a single character
fn scan_ch(data : BytesView, c : Byte) -> Int {
if !data.is_empty() && data.unsafe_get(0) == c {
1
} else {
0
}
}
///|
fn scan_while(data : BytesView, f : (Byte) -> Bool) -> Int {
let mut i = 0
while i < data.length() && f(data.unsafe_get(i)) {
i += 1
}
i
}
///|
fn scan_while_max(data : BytesView, f : (Byte) -> Bool, m : Int) -> Int {
let mut i = 0
while i < data.length() && i < m && f(data.unsafe_get(i)) {
i += 1
}
i
}
///|
fn scan_rev_while(data : BytesView, f : (Byte) -> Bool) -> Int {
let mut i = data.length()
while i > 0 && f(data.unsafe_get(i - 1)) {
i -= 1
}
data.length() - i
}
///|
fn scan_ch_repeat(data : BytesView, c : Byte) -> Int {
scan_while(data, fn(x) { x == c })
}
// Note: this scans ASCII whitespace only, for Unicode whitespace use
// a different function.
///|
fn scan_whitespace_no_nl(data : BytesView) -> Int {
scan_while(data, fn(c) { c.is_ascii_whitespace_no_nl() })
}
///|
fn scan_eol(bytes : BytesView) -> Int? {
guard !bytes.is_empty() else { return Some(0) }
let c = bytes.unsafe_get(0)
guard c != b'\n' else { return Some(1) }
guard c != b'\r' else {
if bytes.length() >= 2 && bytes.unsafe_get(1) == b'\n' {
return Some(2)
}
return Some(1)
}
None
}
///|
fn scan_blank_line(bytes : BytesView) -> Int? {
let i = scan_whitespace_no_nl(bytes)
scan_eol(bytes.view(start=i)).map(n => i + n)
}
///|
fn scan_nextline(bytes : BytesView) -> Int {
// Per CommonMark, a line ending is LF, CRLF, or a lone CR.
let mut i = 0
while i < bytes.length() {
let c = bytes.unsafe_get(i)
guard c != b'\n' else { return i + 1 }
if c == b'\r' {
guard !(bytes.length() > i + 1 && bytes.unsafe_get(i + 1) == b'\n') else {
return i + 2
}
return i + 1
}
i += 1
}
bytes.length()
}
// return: end byte for closing code fence, or None
// if the line is ! a closing code fence
///|
fn scan_closing_code_fence(
bytes : BytesView,
fence_char : Byte,
n_fence_char : Int,
) -> Int? {
guard !bytes.is_empty() else { return Some(0) }
let num_fence_chars_found = scan_ch_repeat(bytes, fence_char)
guard num_fence_chars_found >= n_fence_char else { return None }
let mut i = num_fence_chars_found
let num_trailing_spaces = scan_ch_repeat(bytes.view(start=i), b' ')
i += num_trailing_spaces
scan_eol(bytes.view(start=i)).map(fn(_) { i })
}
// return: end byte for closing metadata block, or None
// if the line is ! a closing metadata block
///|
fn scan_closing_metadata_block(bytes : BytesView, fence_char : Byte) -> Int? {
let mut num_fence_chars_found = scan_ch_repeat(bytes, fence_char)
if num_fence_chars_found != 3 {
// if YAML style metadata block the closing character can also be `.`
guard fence_char == b'-' else { return None }
num_fence_chars_found = scan_ch_repeat(bytes, b'.')
guard num_fence_chars_found == 3 else { return None }
}
let mut i = num_fence_chars_found
let num_trailing_spaces = scan_ch_repeat(bytes.view(start=i), b' ')
i += num_trailing_spaces
scan_eol(bytes.view(start=i)).map(fn(_) { i })
}
// returned pair is (number of bytes, number of spaces)
///|
fn calc_indent(text : BytesView, max : Int) -> (Int, Int) {
let mut spaces = 0
let mut offset = 0
let mut i = 0
while i < text.length() {
offset = i
let b = text.unsafe_get(i)
if b == b' ' {
spaces += 1
if spaces == max {
break
}
} else if b == b'\t' {
let new_spaces = spaces + 4 - (spaces & 3)
if new_spaces > max {
break
}
spaces = new_spaces
} else {
break
}
i += 1
}
(offset, spaces)
}
///|
/// Scan hrule opening sequence.
///
/// Returns Ok(x) when it finds an hrule, where x is the
/// size of line containing the hrule, including the trailing newline.
///
/// Returns Err(x) when it does ! find an hrule and x is
/// the offset in data before no hrule can appear.
fn scan_hrule(bytes : BytesView) -> Result[Int, Int] {
guard bytes.length() >= 3 else { return Err(0) }
let c = bytes.unsafe_get(0)
guard c == b'*' || c == b'-' || c == b'_' else { return Err(0) }
let mut n = 0
let mut i = 0
while i < bytes.length() {
let b = bytes.unsafe_get(i)
if b == b'\n' || b == b'\r' {
i += match scan_eol(bytes.view(start=i)) {
Some(e) => e
None => 0
}
break
} else if b == c {
n += 1
} else if b == b' ' || b == b'\t' {
()
} else {
return Err(i)
}
i += 1
}
guard n >= 3 else { Err(i) }
Ok(i)
}
///|
/// Scan an ATX heading opening sequence.
///
/// Returns number of bytes in prefix and level.
fn scan_atx_heading(data : BytesView) -> HeadingLevel? {
let level = scan_ch_repeat(data, b'#')
let ok = if level < data.length() {
data.unsafe_get(level).to_char().is_ascii_whitespace()
} else {
true
}
if ok {
int_to_heading_level(level)
} else {
None
}
}
///|
/// Scan a setext heading underline.
///
/// Returns number of bytes in line (including trailing newline) and level.
fn scan_setext_heading(data : BytesView) -> (Int, HeadingLevel)? {
guard !data.is_empty() else { return None }
let c = data.unsafe_get(0)
let level = if c == b'=' { H1 } else if c == b'-' { H2 } else { return None }
let mut i = 1 + scan_ch_repeat(data.view(start=1), c)
match scan_blank_line(data.view(start=i)) {
Some(n) => {
i += n
Some((i, level))
}
None => None
}
}
// returns number of bytes in line (including trailing
// newline) and column alignments
///|
fn scan_table_head(data : BytesView) -> (Int, Array[Alignment]) {
let (i0, spaces) = calc_indent(data, 4)
let mut i = i0
guard !(spaces > 3 || i == data.length()) else { return (0, []) }
let cols : Array[Alignment] = []
let mut active_col : Alignment = None
let mut start_col = true
let mut found_pipe = false
let mut found_hyphen = false
let mut found_hyphen_in_col = false
if data.unsafe_get(i) == b'|' {
i += 1
found_pipe = true
}
while i < data.length() {
match scan_eol(data.view(start=i)) {
Some(n) => {
i += n
break
}
None => ()
}
let c = data.unsafe_get(i)
if c == b' ' {
()
} else if c == b':' {
active_col = match (start_col, active_col) {
(true, None) => Left
(false, Left) => Center
(false, None) => Right
_ => active_col
}
start_col = false
} else if c == b'-' {
start_col = false
found_hyphen = true
found_hyphen_in_col = true
} else if c == b'|' {
start_col = true
found_pipe = true
cols.push(active_col)
active_col = None
guard found_hyphen_in_col else {
// It isn't a table head if it has back-to-back pipes.
return (0, [])
}
found_hyphen_in_col = false
} else {
// It isn't a table head if it has characters outside the allowed set.
return (0, [])
}
i += 1
}
if !start_col {
cols.push(active_col)
}
guard found_pipe && found_hyphen else {
// It isn't a table head if it doesn't have at least one pipe or hyphen.
return (0, [])
}
(i, cols)
}
///|
/// Scan code fence.
///
/// Returns number of bytes scanned and the char that is repeated to make the
/// code fence.
fn scan_code_fence(data : BytesView) -> (Int, Byte)? {
guard !data.is_empty() else { return None }
let c = data.unsafe_get(0)
guard c == b'`' || c == b'~' else { return None }
let i = 1 + scan_ch_repeat(data.view(start=1), c)
if i >= 3 {
if c == b'`' {
let suffix = data.view(start=i)
let next_line = i + scan_nextline(suffix)
// FIXME: make sure this is correct
let mut j = 0
while j < next_line - i {
guard suffix.unsafe_get(j) != b'`' else { return None }
j += 1
}
}
Some((i, c))
} else {
None
}
}
///|
fn scan_interrupting_container_extensions_fence(data : BytesView) -> Bool {
let fence_length = scan_ch_repeat(data, b':')
let kind_start = fence_length +
scan_whitespace_no_nl(data.view(start=fence_length))
let kind_length = scan_while(data.view(start=kind_start), fn(c) {
c.is_ascii_alphanumeric() ||
c == b'_' ||
c == b'-' ||
c == b':' ||
c == b'.'
})
fence_length > 2 && kind_length > 0
}
///|
/// Scan metadata block, returning the number of delimiter bytes
/// (always 3 for now) and the delimiter character.
fn scan_metadata_block(
data : BytesView,
yaml_style_enabled : Bool,
pluses_style_enabled : Bool,
) -> (Int, Byte)? {
// Only if metadata blocks are enabled
guard yaml_style_enabled || pluses_style_enabled else { None }
guard !data.is_empty() else { return None }
let c = data.unsafe_get(0)
guard (c == b'-' && yaml_style_enabled) || (c == b'+' && pluses_style_enabled) else {
return None
}
let i = 1 + scan_ch_repeat(data.view(start=1), c)
// Only trailing spaces after the delimiters in the line
let next_line = scan_nextline(data.view(start=i))
let mut j = 0
while j < next_line {
guard data.unsafe_get(i + j).to_char().is_ascii_whitespace() else {
return None
}
j += 1
}
guard i == 3 else { None }
// Search the closing sequence
let mut j = i
let mut first_line = true
while j < data.length() {
j += scan_nextline(data.view(start=j))
let closed = scan_closing_metadata_block(data.view(start=j), c) is Some(_)
// The first line of the metadata block cannot be an empty line
// nor the end of the block
if first_line {
guard !(closed || scan_blank_line(data.view(start=j)) is Some(_)) else {
return None
}
first_line = false
}
guard !closed else { return Some((i, c)) }
}
None
}
///|
fn scan_blockquote_start(data : BytesView) -> Int? {
guard !data.is_empty() && data.unsafe_get(0) == b'>' else { None }
let space = if data.length() >= 2 && data.unsafe_get(1) == b' ' {
1
} else {
0
}
Some(1 + space)
}
///|
/// return number of bytes scanned, delimiter, start index, and indent
fn scan_listitem(bytes : BytesView) -> (Int, Byte, Int, Int)? {
guard !bytes.is_empty() else { return None }
let mut c = bytes.unsafe_get(0)
let (w, start) = if c == b'-' || c == b'+' || c == b'*' {
(1, 0)
} else if c >= b'0' && c <= b'9' {
let (length, start) = parse_decimal(bytes, 9)
guard length < bytes.length() else { return None }
c = bytes.unsafe_get(length)
guard c == b'.' || c == b')' else { return None }
(length + 1, start)
} else {
return None
}
// TODO: replace calc_indent with scan_leading_whitespace, for tab correctness
let (postn0, postindent0) = calc_indent(bytes.view(start=w), 5)
let mut postn = postn0
let mut postindent = postindent0
if postindent == 0 {
match scan_eol(bytes.view(start=w)) {
Some(_) => ()
None => return None
}
postindent += 1
} else if postindent > 4 {
postn = 1
postindent = 1
}
if scan_blank_line(bytes.view(start=w)) is Some(_) {
postn = 0
postindent = 1
}
Some((w + postn, c, start, w + postindent))
}
// returns (number of bytes, parsed decimal)
///|
fn parse_decimal(bytes : BytesView, limit : Int) -> (Int, Int) {
let mut count = 0
let mut acc = 0
while count < bytes.length() && count < limit {
let b = bytes.unsafe_get(count)
if !b.to_char().is_ascii_digit() {
break
}
let digit = b.to_int() - 0x30
let new_acc = acc * 10 + digit
if new_acc < acc {
// overflow
break
}
acc = new_acc
count += 1
}
(count, acc)
}
// returns (number of bytes, parsed hex)
///|
fn parse_hex(bytes : BytesView, limit : Int) -> (Int, Int) {
let mut count = 0
let mut acc = 0
while count < bytes.length() && count < limit {
let mut c = bytes.unsafe_get(count).to_int()
let digit = if c >= 0x30 && c <= 0x39 {
c - 0x30
} else {
// make lower case
c = c | 0x20
if c >= 0x61 && c <= 0x66 {
c - 0x61 + 10
} else {
break
}
}
let new_acc = acc * 16 + digit
if new_acc < acc {
break
}
acc = new_acc
count += 1
}
(count, acc)
}
///|
fn char_from_codepoint(input : Int) -> Char? {
// Codepoint 0 is treated as invalid (emits U+FFFD).
guard input != 0 else { return None }
Int::to_char(input)
}
// doesn't bother to check data[0] == '&'
///|
fn scan_entity(bytes : BytesView) -> (Int, String?) {
let mut end = 1
if bytes.length() > end && bytes.unsafe_get(end) == b'#' {
end += 1
let is_hex = end < bytes.length() &&
(bytes.unsafe_get(end).to_int() | 0x20) == 0x78
let (bytecount, codepoint) = if is_hex {
end += 1
parse_hex(bytes.view(start=end), 6)
} else {
parse_decimal(bytes.view(start=end), 7)
}
end += bytecount
if bytecount == 0 || bytes.length() <= end || bytes.unsafe_get(end) != b';' {
(0, None)
} else {
(
end + 1,
Some(
char_from_codepoint(codepoint).map_or("\u{fffd}", fn(c) {
c.to_string()
}),
),
)
}
} else {
end += scan_while(bytes.view(start=end), fn(c) { c.is_ascii_alphanumeric() })
if bytes.length() > end && bytes.unsafe_get(end) == b';' {
match get_entity(bytes.view(start=1, end~)) {
Some(value) => (end + 1, Some(value))
None => (0, None)
}
} else {
(0, None)
}
}
}
///|
fn scan_wikilink_pipe(
data : BytesView,
start_ix : Int,
len : Int,
) -> (Int, Int)? {
let end_ix = (start_ix + len).min(data.length())
let mut i = start_ix
while i < end_ix {
guard data.unsafe_get(i) != b'|' else { return Some((i + 1, i)) }
i += 1
}
None
}
// note: dest returned is raw, still needs to be unescaped
// returns (bytes consumed, dest_start, dest_end)
///|
fn scan_link_dest(
data : BytesView,
start_ix : Int,
max_next : Int,
) -> (Int, Int, Int)? {
let bytes = data.view(start=start_ix)
let mut i = scan_ch(bytes, b'<')
if i != 0 {
// pointy links
while i < bytes.length() {
let c = bytes.unsafe_get(i)
guard !(c == b'\n' || c == b'\r' || c == b'<') else { return None }
guard c != b'>' else { return Some((i + 1, start_ix + 1, start_ix + i)) }
if c == b'\\' &&
i + 1 < bytes.length() &&
is_ascii_punctuation(bytes.unsafe_get(i + 1).to_int()) {
i += 1
}
i += 1
}
None
} else {
// non-pointy links
let mut nest = 0
while i < bytes.length() {
let c = bytes.unsafe_get(i)
if c.to_int() >= 0x00 && c.to_int() <= 0x20 {
break
}
if c == b'(' {
guard nest <= max_next else { return None }
nest += 1
} else if c == b')' {
if nest == 0 {
break
}
nest -= 1
} else if c == b'\\' &&
i + 1 < bytes.length() &&
is_ascii_punctuation(bytes.unsafe_get(i + 1).to_int()) {
i += 1
}
i += 1
}
guard nest == 0 else { return None }
Some((i, start_ix, start_ix + i))
}
}
///|
/// Returns bytes scanned
fn scan_attribute_name(data : BytesView) -> Int? {
guard !data.is_empty() else { return None }
let c = data.unsafe_get(0)
guard c.to_char().is_ascii_alphabetic() || c == b'_' || c == b':' else {
None
}
Some(
1 +
scan_while(data.view(start=1), fn(c) {
c.is_ascii_alphanumeric() ||
c == b'_' ||
c == b'.' ||
c == b':' ||
c == b'-'
}),
)
}
///|
/// State shared between attribute-scanning functions.
priv struct AttrScanState {
buffer : Buffer
mut last_buf_index : Int
}
///|
fn attr_state_new() -> AttrScanState {
{ buffer: Buffer(size_hint=0), last_buf_index: 0 }
}
///|
/// Scans whitespace and possibly newlines according to the
/// behavior defined by the newline handler. When bytes are skipped,
/// all preceding non-skipped bytes are pushed to the buffer.
fn AttrScanState::scan_whitespace_with_newline_handler(
self : AttrScanState,
data : BytesView,
i : Int,
newline_handler : ((BytesView) -> Int)?,
) -> Int? {
let mut i = i
while i < data.length() {
guard data.unsafe_get(i).to_char().is_ascii_whitespace() else {
return Some(i)
}
match scan_eol(data.view(start=i)) {
Some(eol_bytes) =>
match newline_handler {
Some(handler) => {
i += eol_bytes
let skipped_bytes = handler(data.view(start=i))
if skipped_bytes > 0 {
let start = self.last_buf_index
self.buffer.write_bytesview(data.view(start~, end=i))
self.last_buf_index = i + skipped_bytes
}
i += skipped_bytes
}
None => return None
}
None => i += 1
}
}
Some(i)
}
///|
/// Scans whitespace and possible newlines according to the behavior defined
/// by the newline handler.
fn scan_whitespace_with_newline_handler_without_buffer(
data : BytesView,
i : Int,
newline_handler : ((BytesView) -> Int)?,
) -> Int? {
let mut i = i
while i < data.length() {
guard data.unsafe_get(i).to_char().is_ascii_whitespace() else {
return Some(i)
}
match scan_eol(data.view(start=i)) {
Some(eol_bytes) =>
match newline_handler {
Some(handler) => {
i += eol_bytes
let skipped_bytes = handler(data.view(start=i))
i += skipped_bytes
}
None => return None
}
None => i += 1
}
}
Some(i)
}
///|
/// Returns the index immediately following the attribute value on success.
fn AttrScanState::scan_attribute_value(
self : AttrScanState,
data : BytesView,
i : Int,
newline_handler : ((BytesView) -> Int)?,
) -> Int? {
let mut i = i
guard i < data.length() else { return None }
let c = data.unsafe_get(i)
if c == b'"' || c == b'\'' {
let quote = c
i += 1
while i < data.length() {
guard data.unsafe_get(i) != quote else { return Some(i + 1) }
match scan_eol(data.view(start=i)) {
Some(eol_bytes) =>
match newline_handler {
Some(handler) => {
i += eol_bytes
let skipped_bytes = handler(data.view(start=i))
if skipped_bytes > 0 {
let start = self.last_buf_index
self.buffer.write_bytesview(data.view(start~, end=i))
self.last_buf_index = i + skipped_bytes
}
i += skipped_bytes
}
None => return None
}
None => i += 1
}
}
return None
} else {
let is_special = c == b' ' ||
c == b'=' ||
c == b'>' ||
c == b'<' ||
c == b'`' ||
c == b'\n' ||
c == b'\r'
guard !is_special else { return None }
// unquoted attribute value
i += scan_attr_value_chars(data.view(start=i))
}
Some(i)
}
///|
/// Returns the index immediately following the attribute on success.
fn AttrScanState::scan_attribute(
self : AttrScanState,
data : BytesView,
ix : Int,
newline_handler : ((BytesView) -> Int)?,
) -> Int? {
let mut ix = ix
guard scan_attribute_name(data.view(start=ix)) is Some(n) else { return None }
ix += n
let ix_after_attribute = ix
guard scan_whitespace_with_newline_handler_without_buffer(
data, ix, newline_handler,
)
is Some(n) else {
return None
}
ix = n
if data.length() > ix && data.unsafe_get(ix) == b'=' {
guard self.scan_whitespace_with_newline_handler(
data, ix_after_attribute, newline_handler,
)
is Some(n) else {
return None
}
ix = n
ix += 1
guard self.scan_whitespace_with_newline_handler(data, ix, newline_handler)
is Some(n) else {
return None
}
ix = n
guard self.scan_attribute_value(data, ix, newline_handler) is Some(n) else {
return None
}
ix = n
Some(ix)
} else {
// Leave whitespace for next attribute.
Some(ix_after_attribute)
}
}
///|
fn scan_attr_value_chars(data : BytesView) -> Int {
scan_while(data, fn(c) { c.is_valid_unquoted_attr_value_char() })
}
///|
/// Remove backslash escapes and resolve entities
fn unescape(input : Bytes, from : Int, to : Int, is_in_table : Bool) -> String {
let result = StringBuilder::new()
let mut mark = from
let mut i = from
let bytes = input
while i < to {
let c = bytes.unsafe_get(i)
// Tables are special, because they're parsed as-if the tables
// were parsed in a discrete pass, changing `\|` to `|`, and then
// passing the changed string to the inline parser.
let is_table_double_escape = is_in_table &&
c == b'\\' &&
i + 2 < to &&
bytes.unsafe_get(i + 1) == b'\\' &&
bytes.unsafe_get(i + 2) == b'|'
if is_table_double_escape {
// even number of `\`s before pipe
// odd number is handled in the normal way below
write_range(result, bytes, mark, i)
mark = i + 2
i += 3
} else {
let is_escape = c == b'\\' &&
i + 1 < to &&
is_ascii_punctuation(bytes.unsafe_get(i + 1).to_int())
if is_escape {
write_range(result, bytes, mark, i)
mark = i + 1
i += 2
} else if c == b'&' {
match scan_entity(bytes.view(start=i, end=to)) {
(n, Some(value)) => {
write_range(result, bytes, mark, i)
result.write_string(value)
i += n
mark = i
}
_ => i += 1
}
} else if c == b'\r' {
write_range(result, bytes, mark, i)
i += 1
mark = i
} else if c == b'\x00' {
write_range(result, bytes, mark, i)
result.write_string("\u{fffd}")
i += 1
mark = i
} else {
i += 1
}
}
}
write_range(result, bytes, mark, to)
result.to_string()
}
///|
fn write_range(
buf : StringBuilder,
bytes : BytesView,
from : Int,
to : Int,
) -> Unit {
if to > from {
buf.write_stringview(
@utf8.decode_lossy(bytes.view(start=from, end=to)).view(),
)
}
}
///|
/// Assumes `data` is preceded by `<`.
fn starts_html_block_type_6(data : BytesView) -> Bool {
let i = scan_ch(data, b'/')
let tail = data.view(start=i)
let n = scan_while(tail, fn(c) { c.is_ascii_alphanumeric() })
guard is_html_tag(tail.view(start=0, end=n)) else { return false }
// Starting condition says the next byte must be either a space, a tab,
// the end of the line, the string >, or the string />
let tail = tail.view(start=n)
tail.is_empty() ||
tail.unsafe_get(0) == b' ' ||
tail.unsafe_get(0) == b'\t' ||
tail.unsafe_get(0) == b'\r' ||
tail.unsafe_get(0) == b'\n' ||
tail.unsafe_get(0) == b'>' ||
(
tail.length() >= 2 &&
tail.unsafe_get(0) == b'/' &&
tail.unsafe_get(1) == b'>'
)
}
///|
fn is_html_tag(tag : BytesView) -> Bool {
let mut lo = 0
let mut hi = html_tags().length() - 1
while lo <= hi {
let mid = (lo + hi) / 2
let cmp = compare_case_insensitive(
tag,
@utf8.encode(html_tags()[mid]).view(),
)
guard cmp != 0 else { return true }
if cmp < 0 {
hi = mid - 1
} else {
lo = mid + 1
}
}
false
}
///|
fn compare_case_insensitive(a : BytesView, b : BytesView) -> Int {
let n = a.length().min(b.length())
let mut i = 0
while i < n {
let x = a.unsafe_get(i).to_int() | 0x20
let y = b.unsafe_get(i).to_int() | 0x20
guard x == y else { return x - y }
i += 1
}
a.length() - b.length()
}
// sorted for binary search
///|
fn html_tags() -> Array[String] {
[
"address", "article", "aside", "base", "basefont", "blockquote", "body", "caption",
"center", "col", "colgroup", "dd", "details", "dialog", "dir", "div", "dl", "dt",
"fieldset", "figcaption", "figure", "footer", "form", "frame", "frameset", "h1",
"h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", "iframe", "legend",
"li", "link", "main", "menu", "menuitem", "nav", "noframes", "ol", "optgroup",
"option", "p", "param", "search", "section", "summary", "table", "tbody", "td",
"tfoot", "th", "thead", "title", "tr", "track", "ul",
]
}
///|
/// Assumes that `data` starts with `<`.
/// Returns the index into data directly after the html tag on success.
fn scan_html_type_7(data : BytesView) -> Int? {
// Block type html does ! allow for newlines, so we
// do ! pass a newline handler.
match scan_html_block_inner(data, None) {
Some((_span, i)) => scan_blank_line(data.view(start=i)).map(fn(_) { i })
None => None
}
}
///|
/// Assumes that `data` starts with `<`.
/// Returns the number of bytes scanned and the html in case of success.
/// When some bytes were skipped, because the html was split over
/// multiple leafs (e.g. over multiple lines in a blockquote),
/// the html is returned as a vector of bytes.
fn scan_html_block_inner(
data : BytesView,
newline_handler : ((BytesView) -> Int)?,
) -> (Array[Byte], Int)? {
let state = attr_state_new()
let close_tag_bytes = scan_ch(data.view(start=1), b'/')
let l = scan_while(data.view(start=1 + close_tag_bytes), fn(c) {
c.to_char().is_ascii_alphabetic()
})
guard l != 0 else { return None }
let mut i = 1 + close_tag_bytes + l
i += scan_while(data.view(start=i), fn(c) { c.is_ascii_letterdigitdash() })
if close_tag_bytes == 0 {
while true {
let old_i = i
while true {
i += scan_whitespace_no_nl(data.view(start=i))
match scan_eol(data.view(start=i)) {
Some(eol_bytes) => {
guard eol_bytes != 0 else { return None }
match newline_handler {
Some(handler) => {
i += eol_bytes
let skipped_bytes = handler(data.view(start=i))
if skipped_bytes > 0 {
state.buffer.write_bytesview(
data.view(start=state.last_buf_index, end=i),
)
i += skipped_bytes
state.last_buf_index = i
}
}
None => return None
}
}
None => break
}
}
if data.length() > i &&
(data.unsafe_get(i) == b'/' || data.unsafe_get(i) == b'>') {
break
}
guard old_i != i else {
// No whitespace, which is mandatory.
return None
}
guard state.scan_attribute(data, i, newline_handler) is Some(n) else {
return None
}
i = n
}
}
i += scan_whitespace_no_nl(data.view(start=i))
if close_tag_bytes == 0 {
i += scan_ch(data.view(start=i), b'/')
}
guard data.length() > i && data.unsafe_get(i) == b'>' else { None }
i += 1
let buffer = if state.buffer.is_empty() {
[]
} else {
state.buffer.write_bytesview(data.view(start=state.last_buf_index, end=i))
state.buffer.to_bytes().to_array()
}
Some((buffer, i))
}
///|
/// Returns (next_byte_offset, uri, type)
fn scan_autolink(text : BytesView, start_ix : Int) -> (Int, String, LinkType)? {
match scan_uri(text, start_ix) {
Some((bytes, uri)) => Some((bytes, uri, Autolink))
None =>
match scan_email(text, start_ix) {
Some((bytes, uri)) => Some((bytes, uri, Email))
None => None
}
}
}
///|
/// Returns (next_byte_offset, uri)
fn scan_uri(text : BytesView, start_ix : Int) -> (Int, String)? {
let bytes = text.view(start=start_ix)
// scheme's first byte must be an ascii letter
guard !bytes.is_empty() && bytes.unsafe_get(0).to_char().is_ascii_alphabetic() else {
return None
}
let mut i = 1
while i < bytes.length() {
let c = bytes.unsafe_get(i)
i += 1
guard c.is_ascii_alphanumeric() ||
c == b'.' ||
c == b'-' ||
c == b'+' ||
c == b':' else {
return None
}
if c == b':' {
break
}
}
// scheme length must be between 2 and 32 characters long. scheme
// must be followed by colon
guard i >= 3 && i <= 33 else { return None }
while i < bytes.length() {
let c = bytes.unsafe_get(i)
guard c != b'>' else {
return Some(
(
start_ix + i + 1,
@utf8.decode_lossy(text.view(start=start_ix, end=start_ix + i)),
),
)
}
guard !((c.to_int() >= 0x00 && c.to_int() <= 0x20) || c == b'<') else {
return None
}
i += 1
}
None
}
///|
/// Returns (next_byte_offset, email)
fn scan_email(text : BytesView, start_ix : Int) -> (Int, String)? {
// using a regex library would be convenient, but doing it by hand is not too bad
let bytes = text.view(start=start_ix)
let mut i = 0
while i < bytes.length() {
let c = bytes.unsafe_get(i)
i += 1
if c.is_ascii_alphanumeric() {
()
} else {
let is_special = c == b'.' ||
c == b'!' ||
c == b'#' ||
c == b'$' ||
c == b'%' ||
c == b'&' ||
c == b'\'' ||
c == b'*' ||
c == b'+' ||
c == b'/' ||
c == b'=' ||
c == b'?' ||
c == b'^' ||
c == b'_' ||
c == b'`' ||
c == b'{' ||
c == b'|' ||
c == b'}' ||
c == b'~' ||
c == b'-'
if is_special {
()
} else if c == b'@' && i > 1 {
break
} else {
return None
}
}
}
let mut label_ix = i
while true {
let label_start_ix = label_ix
let mut fresh_label = true
while label_ix < bytes.length() {
let c = bytes.unsafe_get(label_ix)
if c.is_ascii_alphanumeric() {
()
} else if c == b'-' && fresh_label {
return None
} else if c == b'-' {
()
} else {
break
}
fresh_label = false
label_ix += 1
}
let too_long = label_ix - label_start_ix > 63
let ends_in_dash = bytes.unsafe_get(label_ix - 1) == b'-'
guard !(label_ix == label_start_ix || too_long || ends_in_dash) else {
return None
}
let has_dot = bytes.length() > label_ix &&
bytes.unsafe_get(label_ix) == b'.'
guard has_dot else {
i = label_ix
break
}
label_ix += 1
}
let has_gt = bytes.length() > i && bytes.unsafe_get(i) == b'>'
guard has_gt else { return None }
Some(
(
start_ix + i + 1,
@utf8.decode_lossy(text.view(start=start_ix, end=start_ix + i)),
),
)
}
///|
/// Scan comment, declaration, or CDATA section, with initial " Int? {
let mut ix = ix
guard ix < bytes.length() else { return None }
let c = bytes.unsafe_get(ix)
ix += 1
if c == b'-' && ix > scan_guard.comment {
// HTML comment needs two hyphens after the !.
guard ix < bytes.length() && bytes.unsafe_get(ix) == b'-' else {
return None
}
// Yes, we're intentionally going backwards.
ix -= 1
while true {
match find_byte(bytes, ix, b'-') {
Some(off) => {
ix = off + 1
scan_guard.comment = ix
let is_close = bytes.length() > ix &&
bytes.unsafe_get(ix) == b'-' &&
bytes.length() > ix + 1 &&
bytes.unsafe_get(ix + 1) == b'>'
guard !is_close else { return Some(ix + 2) }
}
None => break
}
}
None
} else {
let is_cdata = c == b'[' &&
ix < bytes.length() &&
ix + 6 <= bytes.length() &&
bytes.view(start=ix, end=ix + 6).has_prefix(b"CDATA[".view()) &&
ix > scan_guard.cdata
if is_cdata {
ix += 6 // "CDATA[".len()
match find_byte(bytes, ix, b']') {
Some(off) => ix = off
None => ix = bytes.length()
}
let close_brackets = scan_ch_repeat(bytes.view(start=ix), b']')
ix += close_brackets
guard close_brackets != 0 &&
bytes.length() > ix &&
bytes.unsafe_get(ix) == b'>' else {
scan_guard.cdata = ix
None
}
Some(ix + 1)
} else {
let is_decl = c.to_char().is_ascii_alphabetic() &&
ix > scan_guard.declaration
guard is_decl else { None }
match find_byte(bytes, ix, b'>') {
Some(off) => ix = off
None => ix = bytes.length()
}
guard bytes.length() > ix && bytes.unsafe_get(ix) == b'>' else {
scan_guard.declaration = ix
None
}
Some(ix + 1)
}
}
}
///|
fn find_byte(bytes : BytesView, from : Int, b : Byte) -> Int? {
let mut i = from
while i < bytes.length() {
guard bytes.unsafe_get(i) != b else { return Some(i) }
i += 1
}
None
}
///|
/// Scan processing directive, with initial "" already consumed.
/// Returns the next byte offset on success.
fn scan_inline_html_processing(
bytes : BytesView,
ix : Int,
scan_guard : HtmlScanGuard,
) -> Int? {
let mut ix = ix
guard ix > scan_guard.processing else { return None }
while true {
match find_byte(bytes, ix, b'?') {
Some(off) => {
ix = off + 1
guard !(bytes.length() > ix && bytes.unsafe_get(ix) == b'>') else {
return Some(ix + 1)
}
}
None => break
}
}
scan_guard.processing = ix
None
}
// A struct containing information on the reachability of certain inline HTML
// elements.
///|
struct HtmlScanGuard {
mut cdata : Int
mut processing : Int
mut declaration : Int
mut comment : Int
}
///|
fn html_scan_guard_default() -> HtmlScanGuard {
{ cdata: 0, processing: 0, declaration: 0, comment: 0 }
}
///|
fn bytes_find_byte(bytes : Bytes, from : Int, to : Int, b : Byte) -> Int? {
let mut i = from
while i < to {
guard bytes.unsafe_get(i) != b else { return Some(i - from) }
i += 1
}
None
}
///|
fn bytes_contains_str(
bytes : Bytes,
from : Int,
to : Int,
needle : String,
) -> Bool {
let nb = @utf8.encode(needle)
guard nb.length() != 0 else { return true }
let mut i = from
while i + nb.length() <= to {
guard !bytes.view(start=i, end=i + nb.length()).equal_to_bytes(nb) else {
return true
}
i += 1
}
false
}
///|
fn bytesview_iter_rev_position(bytes : BytesView, f : (Byte) -> Bool) -> Int? {
let mut i = bytes.length()
while i > 0 {
i -= 1
guard !f(bytes.unsafe_get(i)) else { return Some(i + 1) }
}
None
}