//! Fifth part of the first pass: headings, footnotes, reference definitions.
///|
/// Parse an ATX heading.
/// Returns index of start of next line.
fn FirstPass::parse_atx_heading(
self : FirstPass,
start : Int,
atx_level : HeadingLevel,
) -> Int {
let mut ix = start
let heading_ix = self.tree.append(Item::{ start, end: 0, body: Root })
ix += atx_level.to_int()
// next char is space or eol (guaranteed by scan_atx_heading)
let bytes = self.text
match scan_eol(bytes.view(start=ix)) {
Some(eol_bytes) => {
self.tree.nodes[heading_ix].item.end = ix + eol_bytes
self.tree.nodes[heading_ix].item.body = Heading(atx_level, None)
return ix + eol_bytes
}
None => ()
}
// skip leading spaces
let skip_spaces = scan_whitespace_no_nl(bytes.view(start=ix))
ix += skip_spaces
// now handle the header text
let header_start = ix
ignore(self.tree.push()) // so that we can set the endpoint later
// trim the trailing attribute block before parsing the entire line
let (end, content_end, attrs) = if self.options.contains(
enable_heading_attributes(),
) {
// the start of the next line is the end of the header
let header_end = header_start +
scan_nextline(bytes.view(start=header_start))
let (content_end, attrs) = self.extract_and_parse_heading_attribute_block(
header_start, header_end,
)
ignore(self.parse_line(ix, Some(content_end), Disabled))
(header_end, content_end, attrs)
} else {
let (line_ix, line_brk) = self.parse_line(ix, None, Disabled)
ix = line_ix
// Backslash at end is actually hard line break
match line_brk {
Some(brk_item) =>
match brk_item.body {
HardBreak(true) =>
self.tree.append_text(brk_item.start, brk_item.end, false)
_ => ()
}
None => ()
}
(ix, ix, None)
}
self.tree.nodes[heading_ix].item.end = end
// remove trailing matter from header text
let mut empty_text_node = false
match self.tree.cur() {
Some(cur_ix) => {
// remove closing of the ATX heading
let header_text = bytes.view(start=header_start, end=content_end)
let mut limit = bytesview_iter_rev_position(
header_text, not_is_newline_space,
).unwrap_or(0)
let closer = bytesview_iter_rev_position(
header_text.view(start=0, end=limit),
not_is_hash,
).unwrap_or(0)
if closer == 0 {
limit = closer
} else {
let spaces = scan_rev_while(header_text.view(start=0, end=closer), fn(
b,
) {
b == b' '
})
if spaces > 0 {
limit = closer - spaces
}
}
// if text is only spaces, then remove them
self.tree.nodes[cur_ix].item.end = limit + header_start
// limit = 0 when text is empty after removing spaces
if limit == 0 {
empty_text_node = true
}
}
None => ()
}
if empty_text_node {
ignore(self.tree.remove_node())
} else {
ignore(self.tree.pop())
}
self.tree.nodes[heading_ix].item.body = attrs.map_or(
Heading(atx_level, None),
fn(attrs) { Heading(atx_level, Some(self.allocs.allocate_heading(attrs))) },
)
end
}
///|
fn not_is_newline_space(b : Byte) -> Bool {
!(b == b'\n' || b == b'\r' || b == b' ')
}
///|
fn not_is_hash(b : Byte) -> Bool {
b != b'#'
}
///|
/// Returns the number of bytes scanned on success.
fn FirstPass::parse_footnote(self : FirstPass, start : Int) -> Int? {
let bytes = self.text.view(start~)
guard bytes.has_prefix(b"[^".view()) else { return None }
let (i, label) = if self.options.options_has_gfm_footnotes() {
// GitHub doesn't allow footnote definition labels to contain line breaks.
match
scan_link_label_rest(self.text, start + 2, None, self.tree.is_in_table()) {
Some((n, label)) => (n, label)
None => return None
}
} else {
match self.parse_refdef_label(start + 2) {
Some((n, label)) => (n, label)
None => return None
}
}
let mut i = i
guard !(self.options.options_has_gfm_footnotes() && label.contains("\n")) else {
// GitHub doesn't allow footnote definition labels to contain line breaks,
// even if they're escaped.
return None
}
i += 2
guard bytes.length() > i && bytes.unsafe_get(i) == b':' else { return None }
i += 1
self.finish_list(start)
match self.tree.peek_up() {
Some(node_ix) => {
let is_footnote = self.tree.nodes[node_ix].item.body
is FootnoteDefinition(_)
if is_footnote {
// finish previous footnote if it's still open
self.pop(start)
}
}
None => ()
}
if self.options.options_has_gfm_footnotes() {
i += scan_whitespace_no_nl(bytes.view(start=i))
}
self.allocs.footdefs[unicase_fold(label)] = { _use_count: 0 }
ignore(
self.tree.append(Item::{
start,
end: 0, // will get set later
body: FootnoteDefinition(self.allocs.allocate_cow(label)),
}),
)
ignore(self.tree.push())
Some(i)
}
///|
/// Tries to parse a reference label, which can be interrupted by new blocks.
fn FirstPass::parse_refdef_label(
self : FirstPass,
start : Int,
) -> (Int, String)? {
scan_link_label_rest(
self.text,
start,
Some(fn(bytes : BytesView) -> Int? {
let line_start = LineStart::new(bytes)
let tree_position = line_start.scan_containers(self.tree, self.options)
let current_container = tree_position == self.tree.spine_len()
guard !line_start.scan_space(4) else {
return Some(line_start.bytes_scanned())
}
let bytes_scanned = line_start.bytes_scanned()
let suffix = bytes.view(start=bytes_scanned)
let interrupt = self.scan_paragraph_interrupt(
suffix, current_container, tree_position,
) ||
(current_container && scan_setext_heading(suffix) is Some(_))
guard !interrupt else { None }
Some(bytes_scanned)
}),
self.tree.is_in_table(),
)
}
///|
/// Returns number of bytes scanned, label and definition on success.
fn FirstPass::parse_refdef_total(
self : FirstPass,
start : Int,
) -> (Int, String, LinkDef)? {
let bytes = self.text.view(start~)
guard bytes.length() > 0 && bytes.unsafe_get(0) == b'[' else { return None }
guard self.parse_refdef_label(start + 1) is Some((i, label)) else {
return None
}
let mut i = i
i += 1
guard bytes.length() > i && bytes.unsafe_get(i) == b':' else { return None }
i += 1
guard self.scan_refdef(start, start + i) is Some((bytecount, link_def)) else {
return None
}
Some((bytecount + i, label, link_def))
}
///|
/// Returns number of bytes and number of newlines
fn FirstPass::scan_refdef_space(
self : FirstPass,
bytes : BytesView,
i : Int,
) -> (Int, Int)? {
let mut i = i
let mut newlines = 0
while true {
let whitespaces = scan_whitespace_no_nl(bytes.view(start=i))
i += whitespaces
match scan_eol(bytes.view(start=i)) {
Some(eol_bytes) => {
i += eol_bytes
newlines += 1
guard newlines <= 1 else { return None }
}
None => break
}
let line_start = LineStart::new(bytes.view(start=i))
let tree_position = line_start.scan_containers(self.tree, self.options)
let current_container = tree_position == self.tree.spine_len()
if !line_start.scan_space(4) {
let suffix = bytes.view(start=i + line_start.bytes_scanned())
let interrupt = self.scan_paragraph_interrupt(
suffix, current_container, tree_position,
) ||
scan_setext_heading(suffix) is Some(_)
guard !interrupt else { return None }
}
i += line_start.bytes_scanned()
}
Some((i, newlines))
}
///|
/// returns (bytelength, title_str)
fn FirstPass::scan_refdef_title(
self : FirstPass,
text : BytesView,
) -> (Int, String)? {
guard text.length() > 0 else { return None }
let c = text.unsafe_get(0)
let closing_delim = if c == b'\'' {
b'\''
} else if c == b'"' {
b'"'
} else if c == b'(' {
b')'
} else {
return None
}
let mut bytecount = 1
let mut linestart = 1
let mut linebuf = StringBuilder::new()
let mut has_linebuf = false
while bytecount < text.length() {
let c = text.unsafe_get(bytecount)
guard !(closing_delim == b')' && c == b'(') else { return None }
if c == b'\n' || c == b'\r' {
if !has_linebuf {
linebuf = StringBuilder::new()
has_linebuf = true
}
linebuf.write_stringview(
@utf8.decode_lossy(text.view(start=linestart, end=bytecount)).view(),
)
linebuf.write_string("\n") // normalize line breaks
bytecount += 1
if c == b'\r' &&
bytecount < text.length() &&
text.unsafe_get(bytecount) == b'\n' {
bytecount += 1
}
let line_start = LineStart::new(text.view(start=bytecount))
let tree_position = line_start.scan_containers(self.tree, self.options)
let current_container = tree_position == self.tree.spine_len()
if !line_start.scan_space(4) {
let suffix = text.view(start=bytecount + line_start.bytes_scanned())
let interrupt = self.scan_paragraph_interrupt(
suffix, current_container, tree_position,
) ||
scan_setext_heading(suffix) is Some(_)
guard !interrupt else { return None }
}
line_start.scan_all_space()
bytecount += line_start.bytes_scanned()
linestart = bytecount
guard scan_blank_line(text.view(start=bytecount)) is None else {
// blank line - not allowed
return None
}
} else if c == b'\\' {
bytecount += 1
if bytecount < text.length() {
let next = text.unsafe_get(bytecount)
if next != b'\r' && next != b'\n' {
bytecount += 1
}
}
} else if c == closing_delim {
let cow = if has_linebuf {
linebuf.write_stringview(
@utf8.decode_lossy(text.view(start=linestart, end=bytecount)).view(),
)
linebuf.to_string()
} else {
@utf8.decode_lossy(text.view(start=linestart, end=bytecount))
}
return Some((bytecount + 1, cow))
} else {
bytecount += 1
}
}
None
}
///|
/// Returns # of bytes and definition.
/// Assumes the label of the reference including colon has already been scanned.
fn FirstPass::scan_refdef(
self : FirstPass,
span_start : Int,
start : Int,
) -> (Int, LinkDef)? {
let bytes = self.text.view(start=0)
// whitespace between label and url (including up to one newline)
guard self.scan_refdef_space(bytes, start) is Some((i, _newlines)) else {
return None
}
let mut i = i
// scan link dest
guard scan_link_dest(self.text, i, 32)
is Some((dest_length, dest_start, dest_end)) else {
return None
}
guard dest_length != 0 else { return None }
let dest = unescape(self.text, dest_start, dest_end, self.tree.is_in_table())
i += dest_length
// no title
let backup = (
i - start,
LinkDef::{ dest, title: None, _span: (span_start, i) },
)
// scan whitespace between dest and label
let (i, newlines) = match self.scan_refdef_space(bytes, i) {
Some((new_i, newlines)) => {
let newlines = if i == self.text.length() {
newlines + 1
} else {
newlines
}
guard !(new_i == i && newlines == 0) else { return None }
guard newlines <= 1 else { return Some(backup) }
(new_i, newlines)
}
None => return Some(backup)
}
let mut i = i
// scan title
match self.scan_refdef_title(bytes.view(start=i)) {
Some((title_length, title_cow)) => {
i += title_length
if scan_blank_line(bytes.view(start=i)) is Some(_) {
let title_bytes = @utf8.encode(title_cow)
let backup_with_title = (
i - start,
LinkDef::{
dest,
title: Some(
unescape(
title_bytes,
0,
title_bytes.length(),
self.tree.is_in_table(),
),
),
_span: (span_start, i),
},
)
return Some(backup_with_title)
}
}
None => ()
}
guard newlines <= 0 else { Some(backup) }
None
}
///|
/// Checks whether we should break a paragraph on the given input.
fn FirstPass::scan_paragraph_interrupt(
self : FirstPass,
bytes : BytesView,
current_container : Bool,
tree_position : Int,
) -> Bool {
guard !scan_paragraph_interrupt_no_table(
bytes,
current_container,
self.options.contains(enable_footnotes()),
self.options.contains(enable_definition_list()),
self.tree,
tree_position,
) else {
return true
}
// Tables with a `|` on the header row are allowed to interrupt paragraphs.
let not_table = !self.options.contains(enable_tables()) ||
bytes.length() <= 0 ||
bytes.unsafe_get(0) != b'|'
guard !not_table else { return false }
// Checking if something's a valid table or not requires looking at two lines.
let mut pipes = 0
let mut next_line_ix = 0
let mut bsesc = false
let mut last_pipe_ix = 0
for i in 0.. (Int, HeadingAttributes?) {
guard self.options.contains(enable_heading_attributes()) else {
return (header_end, None)
}
// extract the trailing attribute block
let header_bytes = self.text.view(start=header_start, end=header_end)
let (content_len, attr_block_range_rel) = extract_attribute_block_content_from_header_text(
header_bytes,
)
let content_end = header_start + content_len
let attrs = match attr_block_range_rel {
Some((r_start, r_end)) =>
parse_inside_attribute_block(
@utf8.decode_lossy(
self.text.view(start=header_start + r_start, end=header_start + r_end),
),
)
None => None
}
(content_end, attrs)
}
///|
/// Checks whether we should break a paragraph on the given input.
fn scan_paragraph_interrupt_no_table(
bytes : BytesView,
current_container : Bool,
has_footnote : Bool,
definition_list : Bool,
tree : Tree[Item],
tree_position : Int,
) -> Bool {
guard scan_eol(bytes) is None else { return true }
guard scan_hrule(bytes) is Err(_) else { return true }
guard scan_atx_heading(bytes) is None else { return true }
guard scan_code_fence(bytes) is None else { return true }
guard !scan_interrupting_container_extensions_fence(bytes) else {
return true
}
guard scan_blockquote_start(bytes) is None else { return true }
let list_interrupt = match scan_listitem(bytes) {
Some((ix, delim, index, _)) =>
!current_container ||
tree.is_in_table() ||
(
(delim == b'*' || delim == b'-' || delim == b'+' || index == 1) &&
scan_blank_line(bytes.view(start=ix)) is None
)
None => false
}
guard !list_interrupt else { return true }
let html_interrupt = bytes.length() > 0 &&
bytes.unsafe_get(0) == b'<' &&
(
get_html_end_tag(bytes.view(start=1)) is Some(_) ||
starts_html_block_type_6(bytes.view(start=1))
)
guard !html_interrupt else { return true }
let deflist_current = current_container &&
tree
.peek_up()
.map_or(false, fn(cur) {
tree.nodes[cur].item.body
is (Paragraph | TightParagraph | MaybeDefinitionListTitle)
})
let deflist_spine_item = if tree_position < tree.spine_len() {
let spine = tree.walk_spine()
tree.nodes[spine[tree_position]].item.body is DefinitionListDefinition(_)
} else {
false
}
let deflist_interrupt = definition_list &&
(deflist_current || deflist_spine_item) &&
bytes.length() > 0 &&
bytes.unsafe_get(0) == b':'
guard !deflist_interrupt else { return true }
let has_foot_prefix = has_footnote && bytes.has_prefix(b"[^".view())
let foot_label_ok = if has_foot_prefix {
match
scan_link_label_rest(
@utf8.encode(@utf8.decode_lossy(bytes.view(start=2))),
0,
None,
tree.is_in_table(),
) {
Some((len, _)) =>
bytes.length() > 2 + len && bytes.unsafe_get(2 + len) == b':'
None => false
}
} else {
false
}
guard !foot_label_ok else { return true }
false
}
///|
/// Assumes `text_bytes` is preceded by `<`.
fn get_html_end_tag(text_bytes : BytesView) -> String? {
let begin_tags = [b"pre", b"style", b"script", b"textarea"]
let end_tags = ["
", "", "", ""]
for idx in 0..'.
let s = text_bytes.unsafe_get(tag_len)
guard !(s.to_char().is_ascii_whitespace() || s == b'>') else {
return Some(end_tags[idx])
}
}
let st_begin_tags = [b"!--", b"?", b"![CDATA["]
let st_end_tags = ["-->", "?>", "]]>"]
for idx in 0.. 1 &&
text_bytes.unsafe_get(0) == b'!' &&
text_bytes.unsafe_get(1).to_char().is_ascii_alphabetic()
guard !is_decl else { Some(">") }
None
}
///|
fn surgerize_tight_list(tree : Tree[Item], list_ix : Int) -> Unit {
let mut list_item = tree.nodes[list_ix].child
while true {
match list_item {
Some(listitem_ix) => {
let mut node_ix = tree.nodes[listitem_ix].child
while true {
match node_ix {
Some(node) => {
match tree.nodes[node].item.body {
Paragraph => tree.nodes[node].item.body = TightParagraph
_ => ()
}
node_ix = tree.nodes[node].next
}
None => break
}
}
list_item = tree.nodes[listitem_ix].next
}
None => break
}
}
}
///|
fn fixup_end_of_definition_list(tree : Tree[Item], list_ix : Int) -> Unit {
let mut list_item = tree.nodes[list_ix].child
let mut previous_list_item : Int? = None
while true {
match list_item {
Some(listitem_ix) =>
match tree.nodes[listitem_ix].item.body {
DefinitionListTitle | DefinitionListDefinition(_) => {
previous_list_item = Some(listitem_ix)
list_item = tree.nodes[listitem_ix].next
}
MaybeDefinitionListTitle => {
tree.nodes[listitem_ix].item.body = Paragraph
break
}
_ => break
}
None => break
}
}
match previous_list_item {
Some(prev) => tree.truncate_to_parent(prev)
None => ()
}
}