///| Inline-level markdown parser
///| Parses emphasis, strong, code, links, images, etc.
///|
/// Check if inlines array contains a link (used for nested link detection)
fn contains_link(inlines : Array[Inline]) -> Bool {
for inline in inlines {
match inline {
Inline::Link(..) | Inline::RefLink(..) => return true
Inline::Emphasis(children~, ..)
| Inline::Strong(children~, ..)
| Inline::Strikethrough(children~, ..) =>
if contains_link(children) {
return true
}
_ => ()
}
}
false
}
///| Trim trailing spaces from the last Text element in inlines array
///|
/// Optimized to avoid allocation when no trimming is needed
fn trim_trailing_space_from_last_text(inlines : Array[Inline]) -> Unit {
guard inlines.length() > 0 else { return }
guard inlines[inlines.length() - 1] is Inline::Text(content~, span~) else {
return
}
let len = content.length()
guard len > 0 else { return }
// Quick check: does it end with space?
guard content.unsafe_get(len - 1) == ' ' else { return }
// Find last non-space character
let mut end = len - 1
while end > 0 && content.unsafe_get(end - 1) == ' ' {
end = end - 1
}
// Now trim
let _ = inlines.pop()
if end > 0 {
inlines.push(
Inline::Text(content=content.unsafe_substring(start=0, end~), span~),
)
}
}
///| Parse inline content from text
///| When strict=true, uses delimiter stack algorithm for full CommonMark compliance
///|
/// When strict=false (default), uses fast single-pass parser
pub fn parse_inlines(text : String, strict? : Bool = false) -> Array[Inline] {
if strict {
// In strict mode, always use delimiter stack for full CommonMark compliance
parse_inlines_with_delimiter_stack(text)
} else {
parse_inlines_fast(text)
}
}
///|
/// Fast path: simple inline parsing without delimiter stack
fn parse_inlines_fast(text : String) -> Array[Inline] {
let scanner = Scanner::new(text)
let parser = InlineParser::new(scanner)
parser.parse()
}
///|
/// Inline parser state
priv struct InlineParser {
scanner : Scanner
}
///|
fn InlineParser::new(scanner : Scanner) -> InlineParser {
{ scanner, }
}
///|
/// Parse all inline content
fn InlineParser::parse(self : InlineParser) -> Array[Inline] {
let inlines : Array[Inline] = []
while not(self.scanner.is_eof()) {
match self.parse_inline() {
Some(inline) => {
// For soft/hard breaks, trim trailing whitespace from previous text
match inline {
Inline::SoftBreak(..) | Inline::HardBreak(..) =>
trim_trailing_space_from_last_text(inlines)
_ => ()
}
inlines.push(inline)
}
None => {
// Consume single character as text
let start = self.scanner.pos
match self.scanner.consume() {
Some(c) => {
// Merge with previous text if possible
let text = String::make(1, c)
match inlines.last() {
Some(Inline::Text(content~, span~)) => {
// Merge with previous text
let _ = inlines.pop()
inlines.push(
Inline::Text(
content=content + text,
span=Span::new(span.from, self.scanner.pos),
),
)
}
_ =>
inlines.push(
Inline::Text(
content=text,
span=Span::new(start, self.scanner.pos),
),
)
}
}
None => break
}
}
}
}
inlines
}
///|
/// Parse a single inline element
fn InlineParser::parse_inline(self : InlineParser) -> Inline? {
let start = self.scanner.pos
match self.scanner.peek() {
// Backslash escape
Some('\\') => self.try_parse_escape(start)
// Code span
Some('`') => self.try_parse_code_span(start)
// Emphasis/Strong with asterisk
Some('*') => self.try_parse_emphasis(start, '*')
// Emphasis/Strong with underscore
Some('_') => self.try_parse_emphasis(start, '_')
// Strikethrough
Some('~') => self.try_parse_strikethrough(start)
// Footnote reference, Link, or Image
Some('[') =>
// Check for footnote reference [^label]
if char_is(self.scanner.peek_at(1), '^') {
match self.try_parse_footnote_reference(start) {
Some(fn_ref) => Some(fn_ref)
None => self.try_parse_link(start)
}
} else {
self.try_parse_link(start)
}
Some('!') => self.try_parse_image(start)
// Autolink
Some('<') => self.try_parse_autolink(start)
// Hard break (two spaces at end of line)
Some(' ') => self.try_parse_hard_break(start)
// Soft break (newline)
Some('\n') => {
self.scanner.advance(1)
// Skip leading whitespace after newline (CommonMark spec)
while char_is(self.scanner.peek(), ' ') {
self.scanner.advance(1)
}
Some(Inline::SoftBreak(span=Span::new(start, self.scanner.pos)))
}
_ => None
}
}
///|
/// Try to parse backslash escape
fn InlineParser::try_parse_escape(self : InlineParser, start : Int) -> Inline? {
self.scanner.advance(1) // Skip backslash
match self.scanner.peek() {
Some('\n') => {
// Backslash + newline = hard break
self.scanner.advance(1)
// Skip leading whitespace after newline (CommonMark spec)
while char_is(self.scanner.peek(), ' ') {
self.scanner.advance(1)
}
Some(
Inline::HardBreak(
style=HardBreakStyle::Backslash,
span=Span::new(start, self.scanner.pos),
),
)
}
Some(c) if is_punctuation(c) => {
// Escaped punctuation becomes literal text
self.scanner.advance(1)
Some(
Inline::Text(
content=String::make(1, c),
span=Span::new(start, self.scanner.pos),
),
)
}
_ =>
// Not a valid escape, return backslash as text
Some(Inline::Text(content="\\", span=Span::new(start, self.scanner.pos)))
}
}
///|
/// Try to parse code span
fn InlineParser::try_parse_code_span(
self : InlineParser,
start : Int,
) -> Inline? {
// Count opening backticks
let backtick_count = self.scanner.count_char('`')
if backtick_count == 0 {
return None
}
self.scanner.advance(backtick_count)
// Find closing backticks
let content_buf = StringBuilder::new()
while not(self.scanner.is_eof()) {
let closing_count = self.scanner.count_char('`')
if closing_count == backtick_count {
// Found matching closing backticks
let content = content_buf.to_string()
self.scanner.advance(closing_count)
// Trim single leading/trailing space if present and content has them
// But NOT if content is entirely spaces
let trimmed = if content.length() >= 2 {
let first = content.get_char(0)
let last = content.get_char(content.length() - 1)
if first == Some(' ') &&
last == Some(' ') &&
not(is_all_spaces(content)) {
content.unsafe_substring(start=1, end=content.length() - 1)
} else {
content
}
} else {
content
}
return Some(
Inline::Code(
content=trimmed,
backtick_count~,
span=Span::new(start, self.scanner.pos),
),
)
} else if closing_count > 0 {
// Wrong number of backticks, include them in content
for i = 0; i < closing_count; i = i + 1 {
content_buf.write_char('`')
}
self.scanner.advance(closing_count)
} else {
// Regular character
match self.scanner.consume() {
Some(c) => content_buf.write_char(c)
None => break
}
}
}
// No closing backticks found, restore position
self.scanner.restore(start)
None
}
///|
/// Try to parse emphasis or strong
fn InlineParser::try_parse_emphasis(
self : InlineParser,
start : Int,
marker_char : Char,
) -> Inline? {
// Count opening markers
let marker_count = self.scanner.count_char(marker_char)
if marker_count == 0 {
return None
}
// Only handle 1 or 2 markers (emphasis or strong)
let is_strong = marker_count >= 2
let consume_count = if is_strong { 2 } else { 1 }
self.scanner.advance(consume_count)
// Check if followed by whitespace (not valid opener)
match self.scanner.peek() {
Some(' ') | Some('\t') | Some('\n') | None => {
self.scanner.restore(start)
return None
}
_ => ()
}
// Parse content until closing marker
let children = self.parse_until_closing_marker(marker_char, consume_count)
if children.is_empty() {
self.scanner.restore(start)
return None
}
let marker = if marker_char == '*' {
EmphasisMarker::Asterisk
} else {
EmphasisMarker::Underscore
}
if is_strong {
Some(
Inline::Strong(
marker~,
children~,
span=Span::new(start, self.scanner.pos),
),
)
} else {
Some(
Inline::Emphasis(
marker~,
children~,
span=Span::new(start, self.scanner.pos),
),
)
}
}
///|
/// Parse content until closing marker
fn InlineParser::parse_until_closing_marker(
self : InlineParser,
marker_char : Char,
marker_count : Int,
) -> Array[Inline] {
let children : Array[Inline] = []
let text_buf = StringBuilder::new()
let text_start = self.scanner.pos
while not(self.scanner.is_eof()) {
// Check for closing marker
let closing_count = self.scanner.count_char(marker_char)
if closing_count >= marker_count {
// Check if preceded by non-whitespace
let prev_char = self.scanner.peek_at(-1)
match prev_char {
Some(' ') | Some('\t') | Some('\n') => () // Not valid closer
_ => {
// Valid closer - flush text and consume markers
if not(text_buf.is_empty()) {
children.push(
Inline::Text(
content=text_buf.to_string(),
span=Span::new(text_start, self.scanner.pos),
),
)
}
self.scanner.advance(marker_count)
return children
}
}
}
// Handle nested inline elements
match self.scanner.peek() {
Some('`') => {
// Flush text before inline
if not(text_buf.is_empty()) {
children.push(
Inline::Text(
content=text_buf.to_string(),
span=Span::new(text_start, self.scanner.pos),
),
)
}
let nested_start = self.scanner.pos
match self.try_parse_code_span(nested_start) {
Some(code) => {
children.push(code)
continue
}
None => ()
}
}
Some('[') => {
if not(text_buf.is_empty()) {
children.push(
Inline::Text(
content=text_buf.to_string(),
span=Span::new(text_start, self.scanner.pos),
),
)
}
let nested_start = self.scanner.pos
match self.try_parse_link(nested_start) {
Some(link) => {
children.push(link)
continue
}
None => ()
}
}
Some('\n') =>
// Newline ends emphasis in most cases
break
_ => ()
}
// Regular character
match self.scanner.consume() {
Some(c) => text_buf.write_char(c)
None => break
}
}
// No closing marker found
[]
}
///|
/// Try to parse strikethrough
fn InlineParser::try_parse_strikethrough(
self : InlineParser,
start : Int,
) -> Inline? {
// Need exactly two tildes
if self.scanner.count_char('~') < 2 {
return None
}
self.scanner.advance(2)
// Parse content until closing ~~
let content_buf = StringBuilder::new()
let content_start = self.scanner.pos
while not(self.scanner.is_eof()) {
if self.scanner.count_char('~') >= 2 {
// Found closing
let content = content_buf.to_string()
if content.is_empty() {
self.scanner.restore(start)
return None
}
self.scanner.advance(2)
let children = [
Inline::Text(
content~,
span=Span::new(content_start, self.scanner.pos - 2),
),
]
return Some(
Inline::Strikethrough(
children~,
span=Span::new(start, self.scanner.pos),
),
)
}
match self.scanner.consume() {
Some('\n') => break // Newline ends strikethrough
Some(c) => content_buf.write_char(c)
None => break
}
}
self.scanner.restore(start)
None
}
///|
/// Try to parse link
fn InlineParser::try_parse_link(self : InlineParser, start : Int) -> Inline? {
self.scanner.advance(1) // Skip [
// Parse link text
let text_buf = StringBuilder::new()
let mut bracket_depth = 1
while not(self.scanner.is_eof()) && bracket_depth > 0 {
match self.scanner.peek() {
Some('[') => {
bracket_depth += 1
text_buf.write_char('[')
self.scanner.advance(1)
}
Some(']') => {
bracket_depth -= 1
if bracket_depth > 0 {
text_buf.write_char(']')
}
self.scanner.advance(1)
}
Some('\\') => {
self.scanner.advance(1)
match self.scanner.consume() {
Some(c) => text_buf.write_char(c)
None => break
}
}
Some(c) => {
text_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
let link_text = text_buf.to_string()
// Check for inline link (url)
if char_is(self.scanner.peek(), '(') {
self.scanner.advance(1)
// Parse URL
let url_buf = StringBuilder::new()
let mut paren_depth = 1
// Skip leading whitespace
let _ = self.scanner.skip_spaces()
// Check for angle-bracketed URL
if char_is(self.scanner.peek(), '<') {
self.scanner.advance(1)
let mut angle_url_has_newline = false
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some('>') => {
self.scanner.advance(1)
break
}
Some('\n') => {
// Newline in angle-bracket URL invalidates the link
angle_url_has_newline = true
break
}
Some(c) => {
url_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
if angle_url_has_newline {
self.scanner.restore(start)
return None
}
} else {
let mut url_has_newline = false
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some('(') => {
paren_depth += 1
url_buf.write_char('(')
self.scanner.advance(1)
}
Some(')') => {
paren_depth -= 1
if paren_depth == 0 {
break
}
url_buf.write_char(')')
self.scanner.advance(1)
}
Some(' ') | Some('\t') => break // End of URL
Some('\n') => {
// Newline in non-angle-bracket URL invalidates the link
url_has_newline = true
break
}
Some(c) => {
url_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
// If URL contains newline, abort link parsing
if url_has_newline {
self.scanner.restore(start)
return None
}
}
let url = url_buf.to_string()
// Skip whitespace (including newlines) before title
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some(' ') | Some('\t') | Some('\n') => self.scanner.advance(1)
_ => break
}
}
// Parse optional title (supports ", ', and () delimiters)
let title = match self.scanner.peek() {
Some('"') | Some('\'') => {
let quote = match self.scanner.consume() {
Some(c) => c
None => '"'
}
let title_buf = StringBuilder::new()
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some(c) if c == quote => {
self.scanner.advance(1)
break
}
Some('\\') => {
self.scanner.advance(1)
match self.scanner.consume() {
Some(c) => title_buf.write_char(c)
None => break
}
}
Some(c) => {
title_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
title_buf.to_string()
}
Some('(') => {
self.scanner.advance(1) // Skip opening (
let title_buf = StringBuilder::new()
let mut paren_depth = 1
while not(self.scanner.is_eof()) && paren_depth > 0 {
match self.scanner.peek() {
Some(')') => {
paren_depth -= 1
if paren_depth == 0 {
self.scanner.advance(1)
break
}
title_buf.write_char(')')
self.scanner.advance(1)
}
Some('(') => {
paren_depth += 1
title_buf.write_char('(')
self.scanner.advance(1)
}
Some('\\') => {
self.scanner.advance(1)
match self.scanner.consume() {
Some(c) => title_buf.write_char(c)
None => break
}
}
Some(c) => {
title_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
title_buf.to_string()
}
_ => ""
}
// Skip whitespace and closing paren
let _ = self.scanner.skip_spaces()
if char_is(self.scanner.peek(), ')') {
self.scanner.advance(1)
// Parse link text as inlines
let children = parse_inlines(link_text)
// CommonMark: Links cannot contain other links
// If link text contains a link, the outer link is invalid
if contains_link(children) {
self.scanner.restore(start)
return None
}
return Some(
Inline::Link(
children~,
url~,
title~,
span=Span::new(start, self.scanner.pos),
),
)
}
}
// Check for reference link [text][ref]
if char_is(self.scanner.peek(), '[') {
self.scanner.advance(1)
let label_buf = StringBuilder::new()
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some(']') => {
self.scanner.advance(1)
break
}
Some(c) => {
label_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
let label = label_buf.to_string()
let children = parse_inlines(link_text)
// CommonMark: Links cannot contain other links
if contains_link(children) {
self.scanner.restore(start)
return None
}
return Some(
Inline::RefLink(
children~,
label~,
span=Span::new(start, self.scanner.pos),
),
)
}
// Not a valid link
self.scanner.restore(start)
None
}
///|
/// Try to parse footnote reference [^label]
fn InlineParser::try_parse_footnote_reference(
self : InlineParser,
start : Int,
) -> Inline? {
self.scanner.advance(1) // Skip [
if not(char_is(self.scanner.peek(), '^')) {
self.scanner.restore(start)
return None
}
self.scanner.advance(1) // Skip ^
// Read label (alphanumeric, -, _)
let label_buf = StringBuilder::new()
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some(']') => break
Some(c) if (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-' ||
c == '_' => {
label_buf.write_char(c)
self.scanner.advance(1)
}
_ => {
self.scanner.restore(start)
return None
}
}
}
let label = label_buf.to_string()
if label.is_empty() {
self.scanner.restore(start)
return None
}
if not(char_is(self.scanner.peek(), ']')) {
self.scanner.restore(start)
return None
}
self.scanner.advance(1) // Skip ]
Some(
Inline::FootnoteReference(label~, span=Span::new(start, self.scanner.pos)),
)
}
///|
/// Try to parse image
fn InlineParser::try_parse_image(self : InlineParser, start : Int) -> Inline? {
self.scanner.advance(1) // Skip !
if not(char_is(self.scanner.peek(), '[')) {
self.scanner.restore(start)
return None
}
self.scanner.advance(1) // Skip [
// Parse alt text
let alt_buf = StringBuilder::new()
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some(']') => {
self.scanner.advance(1)
break
}
Some('\\') => {
self.scanner.advance(1)
match self.scanner.consume() {
Some(c) => alt_buf.write_char(c)
None => break
}
}
Some(c) => {
alt_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
let alt = alt_buf.to_string()
// Check for inline image (url)
if char_is(self.scanner.peek(), '(') {
self.scanner.advance(1)
// Parse URL (same as link)
let url_buf = StringBuilder::new()
let _ = self.scanner.skip_spaces()
if char_is(self.scanner.peek(), '<') {
self.scanner.advance(1)
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some('>') => {
self.scanner.advance(1)
break
}
Some(c) => {
url_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
} else {
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some(')') | Some(' ') | Some('\t') => break
Some(c) => {
url_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
}
let url = url_buf.to_string()
let _ = self.scanner.skip_spaces()
// Parse optional title
let title = match self.scanner.peek() {
Some('"') | Some('\'') => {
let quote = match self.scanner.consume() {
Some(c) => c
None => '"'
}
let title_buf = StringBuilder::new()
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some(c) if c == quote => {
self.scanner.advance(1)
break
}
Some(c) => {
title_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
title_buf.to_string()
}
_ => ""
}
let _ = self.scanner.skip_spaces()
if char_is(self.scanner.peek(), ')') {
self.scanner.advance(1)
return Some(
Inline::Image(
alt~,
url~,
title~,
span=Span::new(start, self.scanner.pos),
),
)
}
}
// Check for reference image ![alt][ref]
if char_is(self.scanner.peek(), '[') {
self.scanner.advance(1)
let label_buf = StringBuilder::new()
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some(']') => {
self.scanner.advance(1)
break
}
Some(c) => {
label_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
return Some(
Inline::RefImage(
alt~,
label=label_buf.to_string(),
span=Span::new(start, self.scanner.pos),
),
)
}
self.scanner.restore(start)
None
}
///|
/// Try to parse autolink
fn InlineParser::try_parse_autolink(
self : InlineParser,
start : Int,
) -> Inline? {
self.scanner.advance(1) // Skip <
let url_buf = StringBuilder::new()
let mut is_email = false
while not(self.scanner.is_eof()) {
match self.scanner.peek() {
Some('>') => {
self.scanner.advance(1)
let url = url_buf.to_string()
// Check if it's an email
if url.contains("@") && not(url.has_prefix("http")) {
is_email = true
}
return Some(
Inline::Autolink(
url~,
is_email~,
span=Span::new(start, self.scanner.pos),
),
)
}
Some(' ') | Some('\t') | Some('\n') => {
// Not a valid autolink
self.scanner.restore(start)
return None
}
Some(c) => {
url_buf.write_char(c)
self.scanner.advance(1)
}
None => break
}
}
self.scanner.restore(start)
None
}
///|
/// Try to parse hard break (two or more spaces followed by newline)
fn InlineParser::try_parse_hard_break(
self : InlineParser,
start : Int,
) -> Inline? {
let space_count = self.scanner.count_char(' ')
if space_count < 2 {
return None
}
self.scanner.advance(space_count)
if char_is(self.scanner.peek(), '\n') {
self.scanner.advance(1)
// Skip leading whitespace after newline (CommonMark spec)
while char_is(self.scanner.peek(), ' ') {
self.scanner.advance(1)
}
return Some(
Inline::HardBreak(
style=HardBreakStyle::TwoSpaces,
span=Span::new(start, self.scanner.pos),
),
)
}
// Not followed by newline, restore and return None
self.scanner.restore(start)
None
}
///|
/// Check if a string is entirely space characters
fn is_all_spaces(s : String) -> Bool {
for c in s {
if c != ' ' {
return false
}
}
true
}
// ============================================================================
// Delimiter Stack Algorithm for Complex Emphasis
// ============================================================================
///|
/// Delimiter entry for the stack algorithm
priv struct Delimiter {
pos : Int // Position in text
marker : Char // '*' or '_'
length : Int // Number of consecutive markers
mut remaining : Int // Remaining markers not yet matched
can_open : Bool // Can this be an opener?
can_close : Bool // Can this be a closer?
mut active : Bool // Is this delimiter still active?
}
///|
/// Parse inlines using delimiter stack algorithm (CommonMark spec)
fn parse_inlines_with_delimiter_stack(text : String) -> Array[Inline] {
let chars : Array[Char] = text.to_array()
let len = chars.length()
// Phase 1: Collect all delimiters
let delimiters : Array[Delimiter] = []
let mut i = 0
while i < len {
let c = chars[i]
if c == '*' || c == '_' {
// Count consecutive markers
let start = i
let marker = c
let mut count = 0
while i < len && chars[i] == marker {
count += 1
i += 1
}
// Determine flanking status (CommonMark rules)
let before = if start > 0 { Some(chars[start - 1]) } else { None }
let after = if i < len { Some(chars[i]) } else { None }
let before_is_whitespace = match before {
None => true
Some(ch) => is_unicode_whitespace(ch)
}
let before_is_punctuation = match before {
None => false
Some(ch) => is_unicode_punctuation(ch)
}
let after_is_whitespace = match after {
None => true
Some(ch) => is_unicode_whitespace(ch)
}
let after_is_punctuation = match after {
None => false
Some(ch) => is_unicode_punctuation(ch)
}
// Left-flanking: not followed by whitespace, and either
// (a) not followed by punctuation, or (b) preceded by whitespace or punctuation
let left_flanking = not(after_is_whitespace) &&
(
not(after_is_punctuation) ||
before_is_whitespace ||
before_is_punctuation
)
// Right-flanking: not preceded by whitespace, and either
// (a) not preceded by punctuation, or (b) followed by whitespace or punctuation
let right_flanking = not(before_is_whitespace) &&
(
not(before_is_punctuation) ||
after_is_whitespace ||
after_is_punctuation
)
// For underscore, additional rules apply
let (can_open, can_close) = if marker == '_' {
// _ can open if left-flanking and (not right-flanking or preceded by punctuation)
// _ can close if right-flanking and (not left-flanking or followed by punctuation)
(
left_flanking && (not(right_flanking) || before_is_punctuation),
right_flanking && (not(left_flanking) || after_is_punctuation),
)
} else {
// * can open if left-flanking
// * can close if right-flanking
(left_flanking, right_flanking)
}
if can_open || can_close {
delimiters.push(Delimiter::{
pos: start,
marker,
length: count,
remaining: count,
can_open,
can_close,
active: true,
})
}
} else {
i += 1
}
}
// Phase 2: Process delimiters to find matching pairs
// Result: list of (opener_pos, closer_pos, marker, is_strong)
let matches : Array[(Int, Int, Char, Bool)] = []
// Process closers from left to right
for closer_idx = 0
closer_idx < delimiters.length()
closer_idx = closer_idx + 1 {
let closer = delimiters[closer_idx]
if not(closer.can_close) || closer.remaining == 0 {
continue
}
// Look back for a matching opener
for opener_idx = closer_idx - 1
opener_idx >= 0
opener_idx = opener_idx - 1 {
let opener = delimiters[opener_idx]
if not(opener.can_open) || opener.remaining == 0 || not(opener.active) {
continue
}
if opener.marker != closer.marker {
continue
}
// Rule: if sum of lengths is multiple of 3 and both can_open and can_close,
// then opener length and closer length must both not be multiples of 3
// (This prevents *foo**bar* from matching incorrectly)
if (opener.can_open && opener.can_close) ||
(closer.can_open && closer.can_close) {
if (opener.length + closer.length) % 3 == 0 {
if opener.length % 3 != 0 || closer.length % 3 != 0 {
continue
}
}
}
// Found a match! Determine if strong or emphasis
let use_count = if opener.remaining >= 2 && closer.remaining >= 2 {
2
} else {
1
}
let is_strong = use_count == 2
// Calculate positions
let opener_end = opener.pos + opener.length - opener.remaining + use_count
let closer_start = closer.pos + (closer.length - closer.remaining)
matches.push(
(opener_end - use_count, closer_start, opener.marker, is_strong),
)
// Update remaining counts
delimiters[opener_idx].remaining = opener.remaining - use_count
delimiters[closer_idx].remaining = closer.remaining - use_count
// Deactivate delimiters between opener and closer
for j = opener_idx + 1; j < closer_idx; j = j + 1 {
delimiters[j].active = false
}
// If more remaining in closer, continue matching
if closer.remaining > 0 {
// Reset closer_idx to re-process this closer
// (Actually we need to decrement to counteract the loop increment)
// But since we modified remaining, the loop will handle it
}
break
}
}
// Phase 3: Build inline elements from matches
// Sort matches by opener position (they should already be mostly sorted)
// Build result by processing text segments (even if no emphasis matches)
let result : Array[Inline] = []
if matches.is_empty() {
// No emphasis matches, but still need to process other inlines correctly
let segment_inlines = parse_segment_simple(text, 0)
for inline in segment_inlines {
result.push(inline)
}
} else {
build_inlines_from_matches(text, chars, matches, result)
}
result
}
///|
/// Build inline elements from delimiter matches
fn build_inlines_from_matches(
text : String,
chars : Array[Char],
matches : Array[(Int, Int, Char, Bool)],
result : Array[Inline],
) -> Unit {
// Sort matches by opener position
let sorted = matches.copy()
sorted.sort_by(fn(a, b) { a.0.compare(b.0) })
// Build nested structure
let len = chars.length()
build_inlines_recursive(
text,
chars,
0,
len,
sorted,
0,
sorted.length(),
result,
)
}
///|
/// Recursively build inline elements
fn build_inlines_recursive(
text : String,
chars : Array[Char],
start : Int,
end : Int,
matches : Array[(Int, Int, Char, Bool)],
match_start : Int,
match_end : Int,
result : Array[Inline],
) -> Unit {
let mut pos = start
for i = match_start; i < match_end; i = i + 1 {
let (opener_pos, closer_pos, marker, is_strong) = matches[i]
// Skip if outside our range or already processed
if opener_pos < start || closer_pos > end || opener_pos < pos {
continue
}
let marker_len = if is_strong { 2 } else { 1 }
// Add text before this emphasis
if opener_pos > pos {
let segment = text.unsafe_substring(start=pos, end=opener_pos)
// Parse segment for other inline elements (code, links, etc.)
let segment_inlines = parse_segment_simple(segment, pos)
for inline in segment_inlines {
result.push(inline)
}
}
// Build children for this emphasis
let children : Array[Inline] = []
let content_start = opener_pos + marker_len
let content_end = closer_pos
// Find nested matches within this emphasis
let nested_start = i + 1
let mut nested_end = i + 1
while nested_end < match_end {
let (np, nc, _, _) = matches[nested_end]
if np >= content_start && nc <= content_end {
nested_end += 1
} else {
break
}
}
if nested_start < nested_end {
// Has nested emphasis
build_inlines_recursive(
text, chars, content_start, content_end, matches, nested_start, nested_end,
children,
)
// Note: nested matches will be skipped naturally by position check
} else {
// No nested emphasis, parse content simply
let content = text.unsafe_substring(start=content_start, end=content_end)
let content_inlines = parse_segment_simple(content, content_start)
for inline in content_inlines {
children.push(inline)
}
}
// Create emphasis or strong node
let em_marker = if marker == '*' {
EmphasisMarker::Asterisk
} else {
EmphasisMarker::Underscore
}
let span = Span::new(opener_pos, closer_pos + marker_len)
if is_strong {
result.push(Inline::Strong(marker=em_marker, children~, span~))
} else {
result.push(Inline::Emphasis(marker=em_marker, children~, span~))
}
pos = closer_pos + marker_len
}
// Add remaining text
if pos < end {
let segment = text.unsafe_substring(start=pos, end~)
let segment_inlines = parse_segment_simple(segment, pos)
for inline in segment_inlines {
result.push(inline)
}
}
}
///|
/// Parse a text segment for non-emphasis inlines (code spans, links, etc.)
fn parse_segment_simple(text : String, offset : Int) -> Array[Inline] {
// For now, just return as text. Full implementation would parse
// code spans, links, etc. here.
let scanner = Scanner::new(text)
let result : Array[Inline] = []
let text_buf = StringBuilder::new()
let mut text_start = 0
while not(scanner.is_eof()) {
let pos = scanner.pos
match scanner.peek() {
Some('`') => {
// Try to parse code span
let backtick_count = scanner.count_char('`')
scanner.advance(backtick_count)
// Find closing backticks
let content_buf = StringBuilder::new()
let mut found_closing = false
while not(scanner.is_eof()) {
let closing_count = scanner.count_char('`')
if closing_count == backtick_count {
found_closing = true
let content = content_buf.to_string()
scanner.advance(closing_count)
// Flush text buffer
if not(text_buf.is_empty()) {
result.push(
Inline::Text(
content=text_buf.to_string(),
span=Span::new(offset + text_start, offset + pos),
),
)
text_buf.reset()
}
// Trim single leading/trailing space if present
let trimmed = if content.length() >= 2 {
let chars = content.to_array()
if chars[0] == ' ' &&
chars[chars.length() - 1] == ' ' &&
not(is_all_spaces(content)) {
content.unsafe_substring(start=1, end=content.length() - 1)
} else {
content
}
} else {
content
}
result.push(
Inline::Code(
content=trimmed,
backtick_count~,
span=Span::new(offset + pos, offset + scanner.pos),
),
)
text_start = scanner.pos
break
} else if closing_count > 0 {
for j = 0; j < closing_count; j = j + 1 {
content_buf.write_char('`')
}
scanner.advance(closing_count)
} else {
match scanner.consume() {
Some(c) => content_buf.write_char(c)
None => break
}
}
}
if not(found_closing) {
// No closing backticks, include opening backticks in text
for j = 0; j < backtick_count; j = j + 1 {
text_buf.write_char('`')
}
// Content was consumed, add it to text
text_buf.write_string(content_buf.to_string())
}
}
Some('[') => {
// Try to parse link or image
let link_parser = InlineParser::new(scanner)
match link_parser.try_parse_link(pos) {
Some(link) => {
// Flush text buffer
if not(text_buf.is_empty()) {
result.push(
Inline::Text(
content=text_buf.to_string(),
span=Span::new(offset + text_start, offset + pos),
),
)
text_buf.reset()
}
result.push(link)
text_start = scanner.pos
}
None => {
text_buf.write_char('[')
scanner.advance(1)
}
}
}
Some('!') =>
// Try to parse image
if char_is(scanner.peek_at(1), '[') {
let img_parser = InlineParser::new(scanner)
match img_parser.try_parse_image(pos) {
Some(img) => {
// Flush text buffer
if not(text_buf.is_empty()) {
result.push(
Inline::Text(
content=text_buf.to_string(),
span=Span::new(offset + text_start, offset + pos),
),
)
text_buf.reset()
}
result.push(img)
text_start = scanner.pos
}
None => {
text_buf.write_char('!')
scanner.advance(1)
}
}
} else {
text_buf.write_char('!')
scanner.advance(1)
}
Some('<') => {
// Try to parse autolink
let auto_parser = InlineParser::new(scanner)
match auto_parser.try_parse_autolink(pos) {
Some(autolink) => {
// Flush text buffer
if not(text_buf.is_empty()) {
result.push(
Inline::Text(
content=text_buf.to_string(),
span=Span::new(offset + text_start, offset + pos),
),
)
text_buf.reset()
}
result.push(autolink)
text_start = scanner.pos
}
None => {
text_buf.write_char('<')
scanner.advance(1)
}
}
}
Some('\n') => {
// Check for hard break (2+ trailing spaces before newline)
let content = text_buf.to_string()
let trimmed = content.trim_end(chars=" ").to_string()
let trailing_spaces = content.length() - trimmed.length()
let is_hard_break = trailing_spaces >= 2
if not(trimmed.is_empty()) {
result.push(
Inline::Text(
content=trimmed,
span=Span::new(
offset + text_start,
offset + pos - trailing_spaces,
),
),
)
}
text_buf.reset()
scanner.advance(1)
// Skip leading spaces after newline
while char_is(scanner.peek(), ' ') {
scanner.advance(1)
}
if is_hard_break {
result.push(
Inline::HardBreak(
style=HardBreakStyle::TwoSpaces,
span=Span::new(
offset + pos - trailing_spaces,
offset + scanner.pos,
),
),
)
} else {
result.push(
Inline::SoftBreak(
span=Span::new(offset + pos, offset + scanner.pos),
),
)
}
text_start = scanner.pos
}
Some(c) => {
text_buf.write_char(c)
scanner.advance(1)
}
None => break
}
}
// Flush remaining text
if not(text_buf.is_empty()) {
result.push(
Inline::Text(
content=text_buf.to_string(),
span=Span::new(offset + text_start, offset + scanner.pos),
),
)
}
result
}