// Leaf blocks
// The matching functions assume the indentation has been stripped.
///|
pub(all) enum HtmlBlockEndCond {
EndStr(String)
EndCond1
EndBlank
EndBlank7
} derive(Debug, Eq)
///|
#deprecated
#doc(hidden)
pub extend HtmlBlockEndCond with @debug.Debug::{to_repr}
///|
#deprecated
#doc(hidden)
pub extend HtmlBlockEndCond with Eq::{not_equal, equal}
///|
pub(all) enum ListType {
/// With the given marker.
Unordered(Char)
/// Starting at given integer, markers ending with given character, i.e. ')' or '.'.
Ordered(Int32, Char)
} derive(Debug, FromJson, ToJson, Eq)
///|
#deprecated
#doc(hidden)
pub extend ListType with @debug.Debug::{to_repr}
///|
#deprecated
#doc(hidden)
pub extend ListType with @json.FromJson::{from_json}
///|
#deprecated
#doc(hidden)
pub extend ListType with ToJson::{to_json}
///|
#deprecated
#doc(hidden)
pub extend ListType with Eq::{not_equal, equal}
///|
pub fn ListType::is_same_type(self : ListType, other : ListType) -> Bool {
(self, other)
is ((Ordered(_, c0), Ordered(_, c1)) | (Unordered(c0), Unordered(c1))) &&
c0 == c1
}
///|
pub(all) enum LineType {
AtxHeadingLine(HeadingLevel, CharCodePos, First, Last)
BlankLine
BlockQuoteLine
FencedCodeBlockLine(First, Last, (First, Last)?)
HtmlBlockLine(HtmlBlockEndCond)
IndentedCodeBlockLine
ListMarkerLine(ListType, Last)
ParagraphLine
SetextUnderlineLine(HeadingLevel, Last)
ThematicBreakLine(Last)
ExtTableRow(Last)
ExtFootnoteLabel(Array[Span], Last, String)
Nomatch
} derive(Debug, Eq)
///|
#deprecated
#doc(hidden)
pub extend LineType with @debug.Debug::{to_repr}
///|
#deprecated
#doc(hidden)
pub extend LineType with Eq::{not_equal, equal}
///|
/// https://spec.commonmark.org/current/#thematic-breaks
pub fn LineType::thematic_break(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
if start > last {
return Nomatch
}
match s[start] {
'-' | '_' | '*' =>
for count = 1, prev = start, k = start + 1 {
break if k > last {
if count < 3 {
Nomatch
} else {
ThematicBreakLine(prev)
}
} else if s[k] == s[prev] {
continue count + 1, k, k + 1
} else if s[k] is (' ' | '\t') {
continue count, prev, k + 1
} else {
Nomatch
}
}
_ => Nomatch
}
}
///|
/// https://spec.commonmark.org/current/#atx-headings
pub fn LineType::atx_heading(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
fn skip_hashes(s : String, last, k) {
for k = k; k <= last && s[k] == '#'; k = k + 1 {
} nobreak {
k
}
}
fn find_end(s : String, last, k) { // Blank on k, last + 1 if blank* [#+] blank*
let after_blank = first_non_blank(s, last~, start=k + 1)
if after_blank > last || s[after_blank] != '#' {
return after_blank
}
let after_hash = skip_hashes(s, last, after_blank + 1)
let after_blank = first_non_blank(s, last~, start=after_hash)
if after_blank > last || after_blank == after_hash {
return after_blank
}
// This could be the beginning of the end, trigger again
after_blank - 1
}
fn content(s : String, last, k) {
for k = k {
guard k <= last else { break k - 1 }
guard s[k] is (' ' | '\t') else { continue k + 1 }
let end1 = find_end(s, last, k)
guard end1 <= last else { break k - 1 }
continue end1
}
}
fn level(s : String, last, acc, k) {
if k > last {
return AtxHeadingLine(acc, k, k, last)
}
if s[k] == '#' {
if acc < 6 {
return level(s, last, acc + 1, k + 1)
} else {
return Nomatch
}
}
let first = first_non_blank(s, last~, start=k)
if first > last {
return AtxHeadingLine(acc, k, last + 1, last) // Empty cases
}
if first == k {
return Nomatch // Need a blank
}
let last = if s[first] != '#' {
content(s, last, first + 1)
} else {
let end1 = find_end(s, last, first - 1) // Start on blank
if end1 > last {
first - 1
} else {
content(s, last, end1)
}
}
AtxHeadingLine(acc, k, first, last)
}
if start > last || s[start] != '#' {
return Nomatch
}
level(s, last, 1, start + 1)
}
///|
/// https://spec.commonmark.org/current/#setext-heading
pub fn LineType::setext_heading_underline(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
let level = fn(c) { 2 - (c == '=').to_int() }
fn underline(s : String, last, start, k) {
if k > last {
return SetextUnderlineLine(level(s[start].unsafe_to_char()), k - 1)
}
if s[k] == s[start] {
return underline(s, last, start, k + 1)
}
guard s[k] is (' ' | '\t') else { return Nomatch }
let end_blank = first_non_blank(s, last~, start=k + 1)
if end_blank > last {
return SetextUnderlineLine(level(s[start].unsafe_to_char()), k - 1)
}
Nomatch
}
if start > last {
return Nomatch
}
if s[start] != '-' && s[start] != '=' {
return Nomatch
}
underline(s, last, start, start + 1)
}
///|
pub fn LineType::fenced_code_block_start(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
fn info(s : String, last, nobt, info_first, k) {
guard k <= last else { Some((info_first, last)) }
let sk = s[k]
guard !(nobt && sk == '`') else { None }
guard sk is (' ' | '\t') else { info(s, last, nobt, info_first, k + 1) }
let after_blank = first_non_blank(s, last~, start=k)
guard after_blank <= last else { Some((info_first, k - 1)) }
info(s, last, nobt, info_first, after_blank)
}
fn fence(s : String, last, fence_first, k) {
for k = k {
if k <= last && s[k] == s[fence_first] {
continue k + 1
}
let fence_last = k - 1
let fcount = fence_last - fence_first + 1
guard fcount >= 3 else { break Some(Nomatch) }
let info = {
let after_blank = first_non_blank(s, last~, start=k)
if after_blank > last {
None
} else {
guard info(s, last, s[fence_first] == '`', after_blank, after_blank)
is Some(i) else {
break None
}
Some(i)
}
}
break Some(FencedCodeBlockLine(fence_first, fence_last, info))
}
}
if start > last {
return Nomatch
}
for k = start {
guard k <= last else { break Nomatch }
if k - start + 1 < 4 && s[k] == ' ' {
continue k + 1
}
guard s[k] is ('~' | '`') else { break Nomatch }
break fence(s, last, k, k + 1).unwrap_or(Nomatch)
}
}
///|
pub(all) enum FencedCodeBlockContinue {
Close(First, Last)
Code
} derive(Debug)
///|
#deprecated
#doc(hidden)
pub extend FencedCodeBlockContinue with @debug.Debug::{to_repr}
///|
/// `fenced_code_block_continue(fence, s, last, start)` indicates
/// whether the fence code continues or closes in the the range
/// [start;last] given the opening `open` which indicates the
/// indent, fence char and number of fence chars.
pub fn FencedCodeBlockContinue::new(
s : String,
fence~ : (Char, Int32),
last~ : CharCodePos,
start~ : CharCodePos,
) -> FencedCodeBlockContinue {
if start > last {
return Code
}
let (fc, fcount) = fence
fn fence(s : String, last, fence_first, k) {
for k = k {
if k <= last && s[k].unsafe_to_char() == fc {
continue k + 1
}
let fence_last = k - 1
guard fence_last - fence_first + 1 >= fcount else { break None } // Not closing
let after_blank = first_non_blank(s, last~, start=k)
guard after_blank > last else { break None }
break Some(Close(fence_first, fence_last))
}
}
for k = start {
guard k <= last else { break Code } // Short blank line
let sk = s[k].unsafe_to_char()
if k - start + 1 < 4 && sk == ' ' {
continue k + 1
}
guard sk == fc else { break Code }
break fence(s, last, k, k + 1).unwrap_or(Code)
}
}
///|
let html_start_cond_1_set : Set[String] = Set::from_array([
"pre", "script", "style", "textarea",
])
///|
let html_start_cond_6_set : Set[String] = Set::from_array([
"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",
])
///|
fn LineType::html_block_start_5(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
let next = start + 3 // 3 first chars checked
let sub = "CDATA["
if start + 8 > last || !s[next:].has_prefix(sub) {
return Nomatch
}
HtmlBlockLine(EndStr("]]>"))
}
///|
fn LineType::html_block_start_2(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
let next = start + 3 // 3 first chars checked
if next > last || s[next] != '-' {
return Nomatch
}
HtmlBlockLine(EndStr("-->"))
}
///|
fn LineType::html_block_start_7_open_tag(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
// Has to be on the same line we fake one and use the inline parser
let line = { pos: line_pos_none, first: start, last, }
guard open_tag(next_line=fn(_x) { None }, s, (), line~, start~)
is Some((_, _, tag_end)) else {
Nomatch
}
let next = first_non_blank(s, last~, start=tag_end + 1)
if next > last {
HtmlBlockLine(EndBlank7)
} else {
Nomatch
}
}
///|
fn LineType::html_block_start_7_close_tag(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
let line = { pos: line_pos_none, first: start, last, }
guard closing_tag(next_line=fn(_x) { None }, s, (), line~, start~)
is Some((_, _, tag_end)) else {
Nomatch
}
let next = first_non_blank(s, last~, start=tag_end + 1)
if next > last {
HtmlBlockLine(EndBlank7)
} else {
Nomatch
}
}
///|
/// https://spec.commonmark.org/current/#html-blocks
pub fn LineType::html_block_start(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
let next = start + 1
if next > last || s[start] != '<' {
return Nomatch
}
match s[next].unsafe_to_char() {
'?' => HtmlBlockLine(EndStr("?>")) // 3
'!' => {
let next = next + 1
if next > last {
return Nomatch
}
match s[next].unsafe_to_char() {
'[' => LineType::html_block_start_5(s, last~, start~)
'-' => LineType::html_block_start_2(s, last~, start~)
c =>
if c.is_ascii_alphabetic() {
HtmlBlockLine(EndStr(">")) // 4
} else {
Nomatch
}
}
}
c => {
guard c.is_ascii_alphabetic() || c == '/' else { return Nomatch }
let tag_first = if c == '/' { next + 1 } else { next }
let tag_last = for i = tag_first {
if i > last || !s[i].unsafe_to_char().is_ascii_alphabetic() {
break i - 1
}
continue i + 1
}
// The tag name is ASCII alphabetic: build it from code units. A
// view boundary at tag_last + 1 could land on an unpaired
// surrogate and abort.
let tag = {
let b = StringBuilder(size_hint=tag_last - tag_first + 1)
for i in tag_first..<=tag_last {
b.write_char(@char.to_ascii_lower(s[i].unsafe_to_char()))
}
b.to_string()
}
let is_open_end = {
let n = tag_last + 1
n > last ||
s[n]
is (32
| 9
// ' ' | '\t' | '>'
| 62)
}
let is_open_close_end = is_open_end ||
(
tag_last + 2 <= last &&
s[tag_last + 1] == '/' &&
s[tag_last + 2] == '>'
)
if c != '/' {
if html_start_cond_1_set.contains(tag) && is_open_end {
HtmlBlockLine(EndCond1) // 1
} else if html_start_cond_6_set.contains(tag) && is_open_close_end {
HtmlBlockLine(EndBlank) // 6
} else {
LineType::html_block_start_7_open_tag(s, last~, start~)
}
} else if html_start_cond_6_set.contains(tag) && is_open_close_end {
HtmlBlockLine(EndBlank) // 6
} else {
LineType::html_block_start_7_close_tag(s, last~, start~)
}
}
}
}
///|
/// https://spec.commonmark.org/current/#html-blocks end condition 1.
fn LineType::html_block_end_cond_1(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> Bool {
let lower_s = s.to_lower()
for k = start {
guard k + 3 <= last else { break false }
guard s[k] == '<' && s[k + 1] == '/' else { continue k + 1 }
let next = k + 2
// Only create the view once the unit at `next` is a known ASCII
// char: a view boundary on an unpaired surrogate aborts.
let is_end_tag = match s[next] {
'p' => lower_s[next:].has_prefix("pre>")
's' =>
if s[k + 3] == 't' {
lower_s[next:].has_prefix("style>")
} else {
lower_s[next:].has_prefix("script>")
}
't' => lower_s[next:].has_prefix("textarea>")
_ => false
}
guard !is_end_tag else { break true }
continue k + 1
}
}
///|
pub fn LineType::html_block_end(
s : String,
end_cond~ : HtmlBlockEndCond,
last~ : CharCodePos,
start~ : CharCodePos,
) -> Bool {
match end_cond {
EndStr(str) => @char.sub_includes(s, affix=str, first=start, last~)
EndCond1 => LineType::html_block_end_cond_1(s, last~, start~)
EndBlank | EndBlank7 => first_non_blank(s, last~, start~) == last + 1
}
}
///|
pub fn LineType::ext_table_row(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
guard start <= last && s[start] == '|' else { Nomatch }
let first = start + 1
let last_nb = last_non_blank(s, first~, start=last)
let before = last_nb - 1
guard last_nb >= first && s[last_nb] == '|' else { Nomatch }
guard before < first || s[before] != '\\' else { Nomatch }
ExtTableRow(last_nb)
}
///|
pub fn LineType::ext_footnote_label(
buf : StringBuilder,
s : String,
line_pos~ : LinePos,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
guard start + 1 <= last && s[start] == '[' && s[start + 1] == '^' else {
Nomatch
}
let rbrack = first_non_escaped_char(']', s, last~, start=start + 2)
let colon = rbrack + 1
guard colon <= last && s[colon] == ':' && colon - start + 1 >= 5 else {
Nomatch
}
// Get the normalized label
let line = { pos: line_pos, first: start, last, }
guard link_label(buf, next_line=fn(_x) { None }, s, (), line~, start~)
is Some((_, spans, _, key)) else {
Nomatch
}
ExtFootnoteLabel(spans, colon, key)
}
///|
/// https://spec.commonmark.org/current/#link-reference-definition
pub fn could_be_link_ref_definition(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> Bool {
if start > last {
return false
}
for k = start {
guard k <= last else { break false }
if k - start + 1 < 4 && s[k] == ' ' {
continue k + 1
}
break s[k] == '['
}
}
// Container blocks
///|
/// https://spec.commonmark.org/current/#list-marker
pub fn LineType::list_marker(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> LineType {
if start > last {
return Nomatch
}
match s[start].unsafe_to_char() {
'-' | '+' | '*' as c => {
let next = start + 1
if next > last ||
(s[next].to_char() is Some(c) && @char.is_ascii_blank(c)) {
ListMarkerLine(Unordered(c), start)
} else {
Nomatch
}
}
'0'..<='9' as c =>
for count = 1, acc = @char.ascii_digit_to_int(c), k = start + 1 {
if k > last || count > 9 {
return Nomatch
}
break match s[k].unsafe_to_char() {
'0'..<='9' as c =>
continue count + 1, acc * 10 + @char.ascii_digit_to_int(c), k + 1
'.' | ')' as c => {
let next = k + 1
if next > last ||
(s[next].to_char() is Some(c) && @char.is_ascii_blank(c)) {
ListMarkerLine(Ordered(acc, c), k)
} else {
Nomatch
}
}
_ => Nomatch
}
}
_ => Nomatch
}
}
///|
pub fn ext_task_marker(
s : String,
last~ : CharCodePos,
start~ : CharCodePos,
) -> (Char, CharCodePos)? {
guard start < last else { None }
guard s[start] == '[' else { None }
let mut next = start + 1
guard @char.at_checked(s, next) is Ok(u) else { None }
next += @char.length_utf16(u.to_int())
guard next <= last && s[next] == ']' else { None }
next += 1
guard next <= last else { Some((u, last)) }
guard s[next] == ' ' else { None }
Some((u, next))
}