///| ATX and setext heading parsing.
///|
/// Try to parse ATX heading (# Heading)
fn BlockParser::try_parse_atx_heading(
self : BlockParser,
start : Int,
) -> Block? {
let leading = self.read_leading_trivia()
// Count # characters
let level = self.scanner.count_char('#')
if level < 1 || level > 6 {
return None
}
self.scanner.advance(level)
// Must be followed by space or end of line
match self.scanner.peek() {
Some(' ') | Some('\t') | Some('\n') | None => ()
_ => return None
}
// Skip space after #
let _ = self.scanner.skip_spaces()
// Read content until end of line or closing hashes
let line = self.scanner.read_line()
// Check for closing hashes
let (content, closing_hashes) = parse_heading_content(line)
// Skip newline
if char_is(self.scanner.peek(), '\n') {
self.scanner.advance(1)
}
let trailing = Trivia::empty()
// Parse inline content
let children = self.parse_inline_content(content)
Some(
Block::Heading(
level~,
style=HeadingStyle::Atx,
children~,
closing_hashes~,
span=Span::new(start, self.scanner.pos),
leading_trivia=leading,
trailing_trivia=trailing,
),
)
}
///|
/// Parse heading content, returning (content, closing_hash_count)
fn parse_heading_content(line : String) -> (String, Int) {
let trimmed = line.trim_end(chars=" \t").to_owned()
// Count trailing #
let mut hash_count = 0
let mut idx = trimmed.length() - 1
while idx >= 0 {
match trimmed.get_char(idx) {
Some('#') => {
hash_count += 1
idx -= 1
}
_ => break
}
}
// Closing hashes must be preceded by space (or be at start)
if hash_count > 0 && idx >= 0 {
match trimmed.get_char(idx) {
Some(' ') | Some('\t') => ()
_ => return (trimmed, 0) // Not valid closing hashes
}
}
if hash_count > 0 {
let content = trimmed
.unsafe_substring(start=0, end=idx + 1)
.trim_end(chars=" \t")
.to_owned()
(content, hash_count)
} else {
(trimmed, 0)
}
}
///|
/// Try to parse setext heading
fn BlockParser::try_parse_setext_heading(
self : BlockParser,
start : Int,
) -> Block? {
let saved = self.scanner.pos
let leading = self.read_leading_trivia()
// Read content line
let content = self.scanner.read_line()
if content.trim(chars=" \t\n\r").is_empty() {
return None
}
if char_is(self.scanner.peek(), '\n') {
self.scanner.advance(1)
} else {
return None // Must have another line
}
// Check for setext underline
let _ = self.scanner.skip_spaces()
let underline_char = match self.scanner.peek() {
Some(c) if c == '=' || c == '-' => c
_ => return None
}
let level = if underline_char == '=' { 1 } else { 2 }
let underline_count = self.scanner.count_char(underline_char)
if underline_count < 1 {
return None
}
self.scanner.advance(underline_count)
// Rest of line must be empty
let _ = self.scanner.skip_spaces()
if !char_is(self.scanner.peek(), '\n') && !self.scanner.is_eof() {
self.scanner.restore(saved)
return None
}
if char_is(self.scanner.peek(), '\n') {
self.scanner.advance(1)
}
let content_str = content.trim(chars=" \t\n\r").to_owned()
let children = self.parse_inline_content(content_str)
Some(
Block::Heading(
level~,
style=HeadingStyle::Setext,
children~,
closing_hashes=0,
span=Span::new(start, self.scanner.pos),
leading_trivia=leading,
trailing_trivia=Trivia::empty(),
),
)
}