///|
/// A configured Markdown processor.
///
/// A processor owns the rule registry used to turn Markdown input into events.
/// Build it from a rule before calling `parse`.
pub struct Processor {
priv plan : PreparedRulePlan
}
///|
/// Creates a processor from `rule`.
pub fn Processor::Processor(rule : Rule) -> Processor {
let registry = RuleRegistry().use_rule(rule)
{ plan: PreparedRulePlan(registry) }
}
///|
/// Parses Markdown input and pushes events into `sink`.
///
/// Core event rules wrap the downstream sink, so transforms can participate in
/// the parse pipeline.
pub fn Processor::parse(self : Processor, input : String, sink : Sink) -> Unit {
let downstream = apply_core_rules(sink, self.plan)
let state = ParseState()
let source = split_lines(input)
if source.may_contain_reference {
collect_reference_definitions_with_block_lines(
source.lines,
source.line_starts,
self.plan,
state,
)
}
parse_block_lines_into(
source.lines,
source.line_starts,
self.plan,
downstream,
state,
)
downstream.finish()
}
///|
fn may_contain_reference_definition(input : String) -> Bool {
let first_label_opener = find_char(input, '[', 0)
first_label_opener >= 0 &&
find_substring(input, "]:", first_label_opener + 1) >= 0
}
///|
fn collect_reference_definitions_with_block_lines(
lines : Array[String],
line_starts : Array[Int],
plan : PreparedRulePlan,
state : ParseState,
) -> Unit {
parse_block_lines_range_into(
lines,
line_starts,
0,
lines.length(),
plan,
Sink(fn(_) { () }),
state,
collect_only=true,
)
}
///|
fn parse_block_lines_into(
lines : Array[String],
line_starts : Array[Int],
plan : PreparedRulePlan,
sink : Sink,
state : ParseState,
collect_only? : Bool = false,
) -> Unit {
parse_block_lines_range_into(
lines,
line_starts,
0,
lines.length(),
plan,
sink,
state,
collect_only~,
)
}
///|
fn parse_block_lines_range_into(
lines : Array[String],
line_starts : Array[Int],
start : Int,
end : Int,
plan : PreparedRulePlan,
sink : Sink,
state : ParseState,
collect_only? : Bool = false,
) -> Unit {
let mut i = start
let stop = if end > lines.length() { lines.length() } else { end }
let inline_rules = if collect_only { [] } else { plan.inline_parse_rules }
let inline_mode = if collect_only {
CustomInlinePipeline
} else {
plan.inline_mode
}
let inline_trigger_chars = if collect_only {
""
} else {
plan.inline_trigger_chars
}
let block_continue_rules = plan.block_continue_rules
let block_start_rules = plan.block_rules
while i < stop {
let ctx = {
lines,
line_starts,
index: i,
registry: plan.registry,
plan,
inline_rules,
inline_mode,
inline_trigger_chars,
state,
collect_only,
}
let mut consumed = try_block_rules(ctx, block_continue_rules, sink)
if consumed == 0 {
consumed = try_block_rules(ctx, block_start_rules, sink)
}
if consumed <= 0 {
consumed = emit_block_rule_result(emit_fallback_text(ctx), sink)
}
i = i + consumed
}
}
///|
fn try_block_rules(
ctx : BlockContext,
rules : Array[RuleEntry],
sink : Sink,
) -> Int {
for rule in rules {
if !block_rule_trigger_matches(ctx, rule) {
continue
}
if rule.block is Some(f) {
let consumed = emit_block_rule_result(f(ctx), sink, fallback=0)
if consumed > 0 {
return consumed
}
}
}
0
}
///|
fn block_rule_trigger_matches(ctx : BlockContext, rule : RuleEntry) -> Bool {
if rule.triggers == "" {
return true
}
let line = ctx.current_line()
if line == "" {
return false
}
let mut i = 0
while i < line.length() && i < 4 && char_at(line, i) == ' ' {
i = i + 1
}
let c = if i >= line.length() {
10
} else if i >= 4 {
32
} else {
char_at(line, i)
}
contains_char(rule.triggers, c)
}
///|
fn emit_block_rule_result(
result : BlockRuleResult,
sink : Sink,
fallback? : Int = 1,
) -> Int {
match result {
Match(lines, fragment) => {
fragment.emit_to(sink)
lines.to_int()
}
NoMatch => fallback
}
}
///|
fn emit_fallback_text(ctx : BlockContext) -> BlockRuleResult {
Match(ConsumedLines(1), EventFragment::text(ctx.current_line()))
}
///|
fn apply_core_rules(sink : Sink, plan : PreparedRulePlan) -> Sink {
let mut current = sink
let rules = plan.core_rules
for i in (rules.length() - 1)>=..0 {
let rule = rules[i]
if rule.core is Some(f) {
current = f(current)
}
}
current
}
///|
priv struct LineSource {
lines : Array[String]
line_starts : Array[Int]
may_contain_reference : Bool
}
///|
fn split_lines(input : String) -> LineSource {
if input.length() == 0 {
return { lines: [], line_starts: [], may_contain_reference: false }
}
if find_char(input, '\n', 0) < 0 {
return {
lines: [expand_leading_tabs(input)],
line_starts: [0],
may_contain_reference: may_contain_reference_definition(input),
}
}
let lines : Array[String] = []
let line_starts : Array[Int] = []
let mut line_start = 0
let mut seen_label_opener = false
let mut may_contain_reference = false
let input_length = input.length()
for i in 0.. String {
let first_tab = for i in 0.. break i
' ' => ()
_ => break -1
}
} nobreak {
-1
}
if first_tab < 0 {
return line
}
let out = StringBuilder::new(size_hint=line.length() + 4)
for i = 0, column = 0; i < line.length(); {
let c = char_at(line, i)
match c {
' ' => {
out <+ " "
continue i + 1, column + 1
}
'\t' => {
let spaces = 4 - column % 4
for _ in 0.. {
out.write_view(line[i:])
break out.to_string()
}
}
} nobreak {
out.to_string()
}
}