use std::collections::HashMap;
use tracing::trace;
use super::{
FOOTNOTE_LINE_RE,
footnote_block_range,
has_existing_footnote_block,
is_definition_continuation,
parse_definition,
rewrite_tokens,
};
use crate::wrap::FenceTracker;
#[derive(Clone)]
pub(super) struct DefinitionLine {
pub(super) index: usize,
pub(super) new_number: usize,
pub(super) line: String,
}
pub(super) struct NumericCandidate {
index: usize,
number: usize,
indent: String,
whitespace: String,
rest: String,
}
pub(super) struct DefinitionUpdates {
pub(super) definitions: Vec<DefinitionLine>,
pub(super) is_definition_line: Vec<bool>,
}
struct DefinitionScanState<'a> {
mapping: &'a mut HashMap<usize, usize>,
next_number: &'a mut usize,
numeric_list_range: Option<(usize, usize)>,
skip_numeric_conversion: bool,
definitions: Vec<DefinitionLine>,
is_definition_line: Vec<bool>,
numeric_candidates: Vec<NumericCandidate>,
}
pub(super) fn definition_segment_end(lines: &[String], start: usize, block_end: usize) -> usize {
let mut idx = start + 1;
while idx < block_end {
let line = &lines[idx];
if parse_definition(line).is_some() {
break;
}
if is_definition_continuation(line) {
idx += 1;
continue;
}
if line.trim().is_empty() {
if idx + 1 < block_end && parse_definition(&lines[idx + 1]).is_some() {
break;
}
idx += 1;
continue;
}
break;
}
idx
}
fn assign_new_number(
mapping: &mut HashMap<usize, usize>,
number: usize,
next_number: &mut usize,
) -> usize {
if let Some(&mapped) = mapping.get(&number) {
mapped
} else {
let assigned = *next_number;
*next_number += 1;
mapping.insert(number, assigned);
assigned
}
}
fn should_convert_numeric_line(
index: usize,
numeric_range: Option<(usize, usize)>,
skip_numeric_conversion: bool,
) -> bool {
if skip_numeric_conversion {
return false;
}
numeric_range.is_some_and(|(start, end)| index >= start && index < end)
}
fn definition_line_from_parts(
index: usize,
parts: super::parsing::DefinitionParts<'_>,
mapping: &mut HashMap<usize, usize>,
next_number: &mut usize,
) -> DefinitionLine {
let new_number = assign_new_number(mapping, parts.number, next_number);
let rewritten_rest = rewrite_tokens(parts.rest, mapping);
let mut line = String::with_capacity(parts.prefix.len() + rewritten_rest.len() + 8);
line.push_str(parts.prefix);
let header = format!("[^{new_number}]:");
line.push_str(&header);
line.push_str(&rewritten_rest);
DefinitionLine {
index,
new_number,
line,
}
}
pub(super) fn numeric_candidate_from_line(line: &str, index: usize) -> Option<NumericCandidate> {
let caps = FOOTNOTE_LINE_RE.captures(line)?;
let indent = caps.name("indent").map_or("", |m| m.as_str()).to_string();
let num_match = caps.name("num")?;
let rest_match = caps.name("rest")?;
let number = num_match.as_str().parse::<usize>().ok()?;
let rest = rest_match.as_str().to_string();
let sep_start = num_match.end().saturating_add(1);
let rest_start = rest_match.start();
if sep_start > rest_start || sep_start > line.len() || rest_start > line.len() {
return None;
}
let whitespace = line[sep_start..rest_start].to_string();
Some(NumericCandidate {
index,
number,
indent,
whitespace,
rest,
})
}
fn collect_scan_updates(lines: &[String], state: &mut DefinitionScanState<'_>) {
let mut fences = FenceTracker::default();
for (index, line) in lines.iter().enumerate() {
let fence = fences.observe_source_line(line);
if fence.is_fence_marker || fence.is_in_fence {
continue;
}
if let Some(parts) = parse_definition(line) {
state.definitions.push(definition_line_from_parts(
index,
parts,
state.mapping,
state.next_number,
));
state.is_definition_line[index] = true;
continue;
}
if !should_convert_numeric_line(
index,
state.numeric_list_range,
state.skip_numeric_conversion,
) {
trace!(
index,
numeric_list_range = ?state.numeric_list_range,
skip_numeric_conversion = state.skip_numeric_conversion,
"skipping numeric footnote candidate conversion"
);
continue;
}
if state.mapping.is_empty() && state.definitions.is_empty() {
trace!(
index,
"skipping numeric footnote candidate before any reference mapping exists"
);
continue;
}
if let Some(candidate) = numeric_candidate_from_line(line, index) {
state.numeric_candidates.push(candidate);
}
}
}
fn finalize_numeric_candidates(state: &mut DefinitionScanState<'_>) {
for candidate in state.numeric_candidates.drain(..) {
let new_number = assign_new_number(state.mapping, candidate.number, state.next_number);
let rewritten_rest = rewrite_tokens(&candidate.rest, state.mapping);
let mut line = String::with_capacity(
candidate.indent.len() + candidate.whitespace.len() + rewritten_rest.len() + 8,
);
line.push_str(&candidate.indent);
let header = format!("[^{new_number}]:");
line.push_str(&header);
line.push_str(&candidate.whitespace);
line.push_str(&rewritten_rest);
state.definitions.push(DefinitionLine {
index: candidate.index,
new_number,
line,
});
state.is_definition_line[candidate.index] = true;
}
}
pub(super) fn collect_definition_updates(
lines: &[String],
mapping: &mut HashMap<usize, usize>,
) -> DefinitionUpdates {
let mut next_number = mapping.values().copied().max().unwrap_or(0) + 1;
let numeric_list_range = footnote_block_range(lines);
let skip_numeric_conversion = numeric_list_range
.as_ref()
.is_some_and(|(start, _)| has_existing_footnote_block(lines, *start));
let mut state = DefinitionScanState {
mapping,
next_number: &mut next_number,
numeric_list_range,
skip_numeric_conversion,
definitions: Vec::new(),
is_definition_line: vec![false; lines.len()],
numeric_candidates: Vec::new(),
};
collect_scan_updates(lines, &mut state);
finalize_numeric_candidates(&mut state);
DefinitionUpdates {
definitions: state.definitions,
is_definition_line: state.is_definition_line,
}
}
pub(super) fn settled_definitions(lines: &[String]) -> Vec<DefinitionLine> {
let mut definitions = Vec::new();
let mut fences = FenceTracker::default();
for (index, line) in lines.iter().enumerate() {
let fence = fences.observe_source_line(line);
if fence.is_fence_marker || fence.is_in_fence {
continue;
}
if let Some(parts) = parse_definition(line) {
definitions.push(DefinitionLine {
index,
new_number: parts.number,
line: line.clone(),
});
}
}
definitions
}
pub(super) fn rewrite_definition_headers(lines: &mut [String], definitions: &[DefinitionLine]) {
for definition in definitions {
lines[definition.index].clone_from(&definition.line);
}
}
#[cfg(test)]
mod tests;