// The scanners. Each one looks at a line (or a paragraph's text) from a given
// offset and answers how many code units the construct it recognises spans, or
// zero when it does not recognise one there. A code unit past the end reads as
// zero, the way a NUL terminator would.
///|
/// The code unit at `i`, or zero when `i` is past the end.
fn cu(text : StringView, i : Int32) -> Int32 {
if i >= 0 && i < text.length() {
text.at(i).to_int()
} else {
0
}
}
///|
fn is_ascii_punct(c : Int32) -> Bool {
(c >= 0x21 && c <= 0x2F) ||
(c >= 0x3A && c <= 0x40) ||
(c >= 0x5B && c <= 0x60) ||
(c >= 0x7B && c <= 0x7E)
}
///|
/// Any ASCII whitespace, the set the reference implementation accepts after a
/// list marker.
fn is_ascii_space(c : Int32) -> Bool {
c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0B || c == 0x0C || c == 0x0D
}
///|
/// What trimming removes from the end of a line.
fn is_blank_char(c : Int32) -> Bool {
c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D || c == 0x0B || c == 0x0C
}
///|
fn is_space_or_tab(c : Int32) -> Bool {
c == 0x20 || c == 0x09
}
///|
fn is_line_end(c : Int32) -> Bool {
c == 0x0A || c == 0x0D
}
///|
fn is_digit(c : Int32) -> Bool {
c >= 0x30 && c <= 0x39
}
///|
fn is_alpha(c : Int32) -> Bool {
(c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A)
}
///|
fn is_alnum(c : Int32) -> Bool {
is_alpha(c) || is_digit(c)
}
///|
/// `[ \t\v\f\r\n]+`.
fn scan_spacechars(text : StringView, start : Int32) -> Int32 {
let mut i = start
while true {
let c = cu(text, i)
if c == 0x20 ||
c == 0x09 ||
c == 0x0B ||
c == 0x0C ||
c == 0x0D ||
c == 0x0A {
i = i + 1
} else {
break
}
}
i - start
}
///|
/// `[#]{1,6} ([ \t]+|[\r\n])`, returning the whole match including the space
/// after the last `#`.
fn scan_atx_heading_start(text : StringView, start : Int32) -> Int32 {
let mut i = start
while cu(text, i) == 0x23 && i - start < 7 {
i = i + 1
}
let hashes = i - start
if hashes < 1 || hashes > 6 {
return 0
}
let c = cu(text, i)
if is_space_or_tab(c) {
while is_space_or_tab(cu(text, i)) {
i = i + 1
}
return i - start
}
if is_line_end(c) {
return i - start
}
0
}
///|
/// `[=]+ [ \t]* [\r\n]` for level one, `[-]+ [ \t]* [\r\n]` for level two.
fn scan_setext_heading_line(text : StringView, start : Int32) -> Int32 {
let c = cu(text, start)
if c != 0x3D && c != 0x2D {
return 0
}
let mut i = start
while cu(text, i) == c {
i = i + 1
}
while is_space_or_tab(cu(text, i)) {
i = i + 1
}
if is_line_end(cu(text, i)) {
if c == 0x3D {
1
} else {
2
}
} else {
0
}
}
///|
/// An opening code fence: three or more backticks not followed by another
/// backtick on the line, or three or more tildes. Returns the number of fence
/// characters.
fn scan_open_code_fence(text : StringView, start : Int32) -> Int32 {
let c = cu(text, start)
if c != 0x60 && c != 0x7E {
return 0
}
let mut i = start
while cu(text, i) == c {
i = i + 1
}
let fence = i - start
if fence < 3 {
return 0
}
while true {
let d = cu(text, i)
if d == 0 {
return 0
} else if is_line_end(d) {
return fence
} else if c == 0x60 && d == 0x60 {
return 0
}
i = i + 1
}
fence
}
///|
/// A closing code fence: three or more of the fence character followed only by
/// spaces. Returns the number of fence characters.
fn scan_close_code_fence(text : StringView, start : Int32) -> Int32 {
let c = cu(text, start)
if c != 0x60 && c != 0x7E {
return 0
}
let mut i = start
while cu(text, i) == c {
i = i + 1
}
let fence = i - start
if fence < 3 {
return 0
}
while is_space_or_tab(cu(text, i)) {
i = i + 1
}
if is_line_end(cu(text, i)) {
fence
} else {
0
}
}
///|
/// Three or more `-`, `*` or `_` on a line of their own, spaces allowed between
/// them. Returns the match length, or zero.
fn scan_thematic_break(text : StringView, start : Int32) -> Int32 {
let c = cu(text, start)
if c != 0x2A && c != 0x5F && c != 0x2D {
return 0
}
let mut i = start
let mut count = 0
while true {
let d = cu(text, i)
if d == c {
count = count + 1
i = i + 1
} else if d == 0x20 || d == 0x09 {
i = i + 1
} else {
break
}
}
if count >= 3 && is_line_end(cu(text, i)) {
i - start
} else {
0
}
}
///|
/// The opening of a footnote definition, `[^label]:`: a caret, a label that
/// holds nothing but a `]`, a space, a tab or a line ending, a colon, and the
/// run of spaces or tabs the reference tolerates after it. Returns the number of
/// code units the opening spans, or zero.
fn scan_footnote_definition(text : StringView, start : Int32) -> Int32 {
if cu(text, start) != 0x5B {
return 0
}
if cu(text, start + 1) != 0x5E {
return 0
}
let n = text.length()
let mut i = start + 2
let mut saw = false
while i < n {
let c = cu(text, i)
if c == 0x5D {
if saw && cu(text, i + 1) == 0x3A {
// The spaces and tabs after the colon belong to the marker, so the
// body begins at the first character that is not one of them.
i = i + 2
while cu(text, i) == 0x20 || cu(text, i) == 0x09 {
i = i + 1
}
return i - start
}
return 0
}
if c == 0x20 || c == 0x09 || c == 0x0A || c == 0x0D {
return 0
}
saw = true
i = i + 1
}
0
}
///|
/// The names the CommonMark specification lists as block level HTML tags.
let html_block_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", "section", "source", "summary", "table", "tbody", "td",
"tfoot", "th", "thead", "title", "tr", "track", "ul",
]
///|
/// Whether `text` at `i` begins with `word`, ignoring ASCII case.
fn starts_with_word(
text : StringView,
i : Int32,
word : StringView,
limit : Int32,
) -> Bool {
if i + word.length() > limit {
return false
}
for k = 0; k < word.length(); k = k + 1 {
let c = cu(text, i + k)
let d = word.at(k).to_int()
let left = if c >= 0x41 && c <= 0x5A { c + 0x20 } else { c }
let right = if d >= 0x41 && d <= 0x5A { d + 0x20 } else { d }
if left != right {
return false
}
}
true
}
///|
/// Which kind of HTML block a line opens, as numbered by the specification, or
/// zero. Type seven is `scan_html_block_start_7`, because it depends on the
/// paragraph it would interrupt.
fn scan_html_block_start(text : StringView, start : Int32) -> Int32 {
if cu(text, start) != 0x3C {
return 0
}
let i = start + 1
let c = cu(text, i)
if c == 0x21 {
// `"[:], n)
3 => contains_word(text, start, "?>"[:], n)
4 => contains_word(text, start, ">"[:], n)
5 => contains_word(text, start, "]]>"[:], n)
_ => false
}
}
///|
/// Whether `needle` appears in `text` at or after `start`, compared ignoring
/// ASCII case.
fn contains_word(
text : StringView,
start : Int32,
needle : StringView,
limit : Int32,
) -> Bool {
let mut i = start
while i < limit {
if starts_with_word(text, i, needle, limit) {
return true
}
i = i + 1
}
false
}
///|
/// `[A-Za-z][A-Za-z0-9-]*`, returning its length.
fn scan_tag_name(text : StringView, start : Int32) -> Int32 {
if !is_alpha(cu(text, start)) {
return 0
}
let mut i = start + 1
while true {
let c = cu(text, i)
if is_alnum(c) || c == 0x2D {
i = i + 1
} else {
break
}
}
i - start
}
///|
/// `[a-zA-Z_:][a-zA-Z0-9:._-]*`, returning its length.
fn scan_attribute_name(text : StringView, start : Int32) -> Int32 {
let c = cu(text, start)
if !(is_alpha(c) || c == 0x5F || c == 0x3A) {
return 0
}
let mut i = start + 1
while true {
let d = cu(text, i)
if is_alnum(d) || d == 0x3A || d == 0x2E || d == 0x5F || d == 0x2D {
i = i + 1
} else {
break
}
}
i - start
}
///|
/// One attribute value, quoted or not, returning its length.
fn scan_attribute_value(text : StringView, start : Int32) -> Int32 {
let c = cu(text, start)
if c == 0x27 || c == 0x22 {
let mut i = start + 1
while true {
let d = cu(text, i)
if d == 0 {
return 0
}
if d == c {
return i + 1 - start
}
i = i + 1
}
}
let mut i = start
while true {
let d = cu(text, i)
if d == 0 ||
d == 0x20 ||
d == 0x09 ||
d == 0x0A ||
d == 0x0B ||
d == 0x0C ||
d == 0x0D ||
d == 0x22 ||
d == 0x27 ||
d == 0x3D ||
d == 0x3C ||
d == 0x3E ||
d == 0x60 {
break
}
i = i + 1
}
i - start
}
///|
/// An open or close tag, starting at the character after `<`, returning the
/// length of the tag up to and including its `>`.
fn scan_html_tag(text : StringView, start : Int32) -> Int32 {
let mut i = start
if cu(text, i) == 0x2F {
// A close tag.
i = i + 1
let name = scan_tag_name(text, i)
if name == 0 {
return 0
}
i = i + name
while is_space_or_tab(cu(text, i)) ||
cu(text, i) == 0x0A ||
cu(text, i) == 0x0C ||
cu(text, i) == 0x0D {
i = i + 1
}
if cu(text, i) == 0x3E {
return i + 1 - start
}
return 0
}
let name = scan_tag_name(text, i)
if name == 0 {
return 0
}
i = i + name
while true {
let before = i
while is_space_or_tab(cu(text, i)) ||
cu(text, i) == 0x0A ||
cu(text, i) == 0x0C ||
cu(text, i) == 0x0D {
i = i + 1
}
let c = cu(text, i)
if c == 0x3E {
return i + 1 - start
}
if c == 0x2F && cu(text, i + 1) == 0x3E {
return i + 2 - start
}
if i == before {
// An attribute has to be separated from the tag name or the one before.
return 0
}
let attribute = scan_attribute_name(text, i)
if attribute == 0 {
return 0
}
i = i + attribute
let after_name = i
while is_space_or_tab(cu(text, i)) ||
cu(text, i) == 0x0A ||
cu(text, i) == 0x0C ||
cu(text, i) == 0x0D {
i = i + 1
}
if cu(text, i) == 0x3D {
i = i + 1
while is_space_or_tab(cu(text, i)) ||
cu(text, i) == 0x0A ||
cu(text, i) == 0x0C ||
cu(text, i) == 0x0D {
i = i + 1
}
let value = scan_attribute_value(text, i)
if value == 0 {
return 0
}
i = i + value
} else {
i = after_name
}
}
abort("unreachable")
}
///|
/// An HTML comment, starting at the character after `<`, including its `-->`.
fn scan_html_comment(text : StringView, start : Int32) -> Int32 {
// The caller has already seen ``.
fn scan_html_pi(text : StringView, start : Int32) -> Int32 {
let mut i = start
while true {
let c = cu(text, i)
if c == 0 {
if i + 2 <= text.length() && cu(text, i) == 0 && i >= text.length() {
return 0
}
return 0
}
if c == 0x3F && cu(text, i + 1) == 0x3E {
return i + 2 - start
}
i = i + 1
}
abort("unreachable")
}
///|
/// A markup declaration, starting at the character after ``.
fn scan_html_declaration(text : StringView, start : Int32) -> Int32 {
let mut i = start
if !(cu(text, i) >= 0x41 && cu(text, i) <= 0x5A) {
return 0
}
while cu(text, i) >= 0x41 && cu(text, i) <= 0x5A {
i = i + 1
}
let spaces = scan_spacechars(text, i)
if spaces == 0 {
return 0
}
i = i + spaces
while true {
let c = cu(text, i)
if c == 0 {
return 0
}
if c == 0x3E {
return i + 1 - start
}
i = i + 1
}
abort("unreachable")
}
///|
/// A CDATA section, starting at the character after ``.
fn scan_html_cdata(text : StringView, start : Int32) -> Int32 {
let mut i = start
if !starts_with_word(text, i, "CDATA[", text.length()) {
return 0
}
i = i + 6
while true {
let c = cu(text, i)
if c == 0 {
return 0
}
if c == 0x5D && cu(text, i + 1) == 0x5D && cu(text, i + 2) == 0x3E {
return i + 3 - start
}
i = i + 1
}
abort("unreachable")
}
///|
/// A link title in quotes or parentheses, including them. One level of escaped
/// quote nesting is allowed.
fn scan_link_title(text : StringView, start : Int32) -> Int32 {
let open = cu(text, start)
let close = match open {
0x22 => 0x22
0x27 => 0x27
0x28 => 0x29
_ => return 0
}
let mut i = start + 1
while true {
let c = cu(text, i)
if c == 0 {
return 0
}
if c == 0x5C && is_ascii_punct(cu(text, i + 1)) {
i = i + 2
continue
}
if c == close {
return i + 1 - start
}
if c == open && open == 0x28 {
return 0
}
i = i + 1
}
abort("unreachable")
}
///|
/// A URI autolink after the `<`, including its `>`.
fn scan_autolink_uri(text : StringView, start : Int32) -> Int32 {
let mut i = start
if !is_alpha(cu(text, i)) {
return 0
}
i = i + 1
let mut scheme = 1
while true {
let c = cu(text, i)
if is_alnum(c) || c == 0x2E || c == 0x2B || c == 0x2D {
i = i + 1
scheme = scheme + 1
} else {
break
}
}
if scheme < 2 || scheme > 32 || cu(text, i) != 0x3A {
return 0
}
i = i + 1
while true {
let c = cu(text, i)
if c == 0 {
return 0
}
if c == 0x3E {
return i + 1 - start
}
if c <= 0x20 || c == 0x3C {
return 0
}
i = i + 1
}
abort("unreachable")
}
///|
/// An email autolink after the `<`, including its `>`.
fn scan_autolink_email(text : StringView, start : Int32) -> Int32 {
let mut i = start
while true {
let c = cu(text, i)
if is_alnum(c) ||
c == 0x2E ||
c == 0x21 ||
c == 0x23 ||
c == 0x24 ||
c == 0x25 ||
c == 0x26 ||
c == 0x27 ||
c == 0x2A ||
c == 0x2B ||
c == 0x2F ||
c == 0x3D ||
c == 0x3F ||
c == 0x5E ||
c == 0x5F ||
c == 0x60 ||
c == 0x7B ||
c == 0x7C ||
c == 0x7D ||
c == 0x7E ||
c == 0x2D {
i = i + 1
} else {
break
}
}
if cu(text, i) != 0x40 {
return 0
}
i = i + 1
// One domain label: alphanumeric, up to 63 characters, no trailing hyphen.
while true {
if !is_alnum(cu(text, i)) {
break
}
let mut j = i + 1
while j - i <= 62 && (is_alnum(cu(text, j)) || cu(text, j) == 0x2D) {
j = j + 1
}
if cu(text, j - 1) == 0x2D {
// Back off the trailing hyphens; the label has to end alphanumeric.
while j - 1 > i && cu(text, j - 1) == 0x2D {
j = j - 1
}
}
i = j
if cu(text, i) == 0x2E {
i = i + 1
if is_alnum(cu(text, i)) {
continue
}
}
break
}
if cu(text, i) == 0x3E {
return i + 1 - start
}
0
}
///|
/// A table's delimiter row: optional leading pipe, then one cell per column of
/// the form `:?-+:?`, separated by pipes, with an optional trailing pipe.
fn scan_table_start(text : StringView, start : Int32) -> Int32 {
let n = text.length()
let mut i = start
if cu(text, i) == 0x7C {
i = i + 1
}
while true {
// One marker.
let column_start = i
while is_space_or_tab(cu(text, i)) {
i = i + 1
}
if cu(text, i) == 0x3A {
i = i + 1
}
let mut hyphens = 0
while cu(text, i) == 0x2D {
hyphens = hyphens + 1
i = i + 1
}
if hyphens == 0 {
return 0
}
if cu(text, i) == 0x3A {
i = i + 1
}
while is_space_or_tab(cu(text, i)) {
i = i + 1
}
if i == column_start {
return 0
}
if cu(text, i) == 0x7C {
i = i + 1
// A trailing pipe may end the row, otherwise another column follows.
let mut j = i
while is_space_or_tab(cu(text, j)) {
j = j + 1
}
if is_line_end(cu(text, j)) {
return j - start
}
continue
}
break
}
if is_line_end(cu(text, i)) {
i - start
} else if i == n {
0
} else {
0
}
}
///|
/// One table cell: everything up to the next unescaped pipe or the end of the
/// line, escapes honoured.
fn scan_table_cell(text : StringView, start : Int32) -> Int32 {
let mut i = start
while true {
let c = cu(text, i)
if c == 0 || c == 0x0A || c == 0x0D || c == 0x7C {
return i - start
}
if c == 0x5C && is_ascii_punct(cu(text, i + 1)) {
i = i + 2
continue
}
i = i + 1
}
abort("unreachable")
}
///|
/// A pipe and the spaces after it.
fn scan_table_cell_end(text : StringView, start : Int32) -> Int32 {
if cu(text, start) != 0x7C {
return 0
}
let mut i = start + 1
while is_space_or_tab(cu(text, i)) ||
cu(text, i) == 0x0B ||
cu(text, i) == 0x0C {
i = i + 1
}
i - start
}
///|
/// The spaces and newline that end a table row.
fn scan_table_row_end(text : StringView, start : Int32) -> Int32 {
let mut i = start
while is_space_or_tab(cu(text, i)) ||
cu(text, i) == 0x0B ||
cu(text, i) == 0x0C {
i = i + 1
}
if is_line_end(cu(text, i)) {
i + 1 - start
} else {
0
}
}