use tracing::trace;
mod block;
mod blockquote;
mod continuation;
mod fence;
mod inline;
mod link_reference;
mod paragraph;
mod passthrough;
mod pending;
mod prefix;
mod tokenize;
#[cfg(test)]
pub(crate) mod tracing_snapshot_support;
pub(crate) use block::{BlockKind, classify_block, classify_residual_block, leading_indent};
pub use blockquote::BlockquotePrefix;
use continuation::apply_continuation_chunk;
pub(crate) use fence::{FenceObservation, ObservedFence};
pub use fence::{FenceTracker, is_fence};
pub(crate) use link_reference::{LinkReferenceMatcher, LinkTitleWindow, LinkTitleWindowOutcome};
use paragraph::{ParagraphState, ParagraphWriter, PrefixLine};
use passthrough::{is_passthrough_block, normalized_passthrough_line};
use pending::handle_pending_continuation;
use prefix::prefix_line;
pub use tokenize::Token;
#[doc(inline)]
pub use tokenize::tokenize_markdown;
#[cfg(test)]
pub(crate) use tokenize::{continuation_begins_with_closing_fence, has_unclosed_code_span};
pub(crate) use tokenize::{has_odd_backslash_escape_bytes, link_or_image_span};
#[derive(Clone, Copy)]
struct LineContext<'a> {
original: &'a str,
inner: &'a str,
blockquote: Option<BlockquotePrefix<'a>>,
block_kind: Option<BlockKind>,
}
#[derive(Clone, Copy)]
struct PreambleLine<'a> {
original: &'a str,
inner: &'a str,
depth: usize,
is_setext: bool,
}
fn line_break_parts(line: &str) -> (String, bool) {
let trimmed_end = line.trim_end();
let text_without_html_breaks = trimmed_end
.trim_end_matches("<br>")
.trim_end_matches("<br/>")
.trim_end_matches("<br />");
let is_trailing_spaces = line.ends_with(" ");
let is_html_br = trimmed_end != text_without_html_breaks;
let backslash_count = trimmed_end.chars().rev().take_while(|&c| c == '\\').count();
let is_backslash_escape = backslash_count % 2 == 1;
let hard_break = is_trailing_spaces || is_html_br || is_backslash_escape;
let text = text_without_html_breaks
.trim_start()
.trim_end_matches(' ')
.to_string();
(text, hard_break)
}
fn try_blockquote_fast_path(
line: LineContext<'_>,
writer: &mut ParagraphWriter<'_>,
state: &mut ParagraphState,
) -> bool {
let outer_matches_pending = line.blockquote.is_some_and(|prefix| {
state
.pending_prefix
.as_ref()
.is_some_and(|pending| pending.outer_prefix.as_deref() == Some(prefix.raw_prefix()))
});
if !outer_matches_pending || line.block_kind.is_some() {
return false;
}
let (text, hard_break) = line_break_parts(line.inner);
apply_continuation_chunk(&text, line.original, hard_break, writer, state);
true
}
fn try_passthrough_block(
line: LineContext<'_>,
writer: &mut ParagraphWriter<'_>,
state: &mut ParagraphState,
link_matcher: LinkReferenceMatcher,
link_title_window: &mut link_reference::LinkTitleWindow,
) -> bool {
if !is_passthrough_block(line.block_kind, line.inner) {
return false;
}
trace!(
?line.block_kind,
line_len = line.original.len(),
"passing a block boundary through unchanged"
);
if matches!(line.block_kind, Some(BlockKind::LinkReferenceDefinition)) {
link_title_window.observe_definition(line.inner, link_matcher);
}
let emitted = normalized_passthrough_line(line.original);
writer.push_verbatim(state, emitted);
true
}
fn handle_line_preamble(
line: PreambleLine<'_>,
writer: &mut ParagraphWriter<'_>,
state: &mut ParagraphState,
fence_tracker: &mut FenceTracker,
link_matcher: LinkReferenceMatcher,
link_title_window: &mut link_reference::LinkTitleWindow,
) -> bool {
if fence::handle_fence_line(
line.original,
line.inner,
line.depth,
writer,
state,
fence_tracker,
) {
link_title_window.observe_fence_context();
return true;
}
if fence_tracker.in_fence(line.depth) {
link_title_window.observe_fence_context();
writer.push_verbatim(state, line.original);
return true;
}
if let Some(outcome) = link_title_window.observe_next_line(line.inner, link_matcher)
&& outcome == link_reference::LinkTitleWindowOutcome::EmitVerbatim
{
writer.push_verbatim(state, line.original);
return true;
}
if line.is_setext {
writer.push_verbatim(state, line.original);
return true;
}
false
}
fn dispatch_continuation(
line: LineContext<'_>,
writer: &mut ParagraphWriter<'_>,
state: &mut ParagraphState,
link_matcher: LinkReferenceMatcher,
link_title_window: &mut link_reference::LinkTitleWindow,
) -> bool {
if state.pending_prefix.is_some()
&& handle_pending_continuation(line, writer, state, link_matcher, link_title_window)
{
return true;
}
if try_passthrough_block(line, writer, state, link_matcher, link_title_window) {
return true;
}
if let Some(prefix_line) = prefix_line(line.inner, line.blockquote) {
writer.handle_prefix_line(state, &prefix_line);
return true;
}
false
}
#[must_use]
pub fn wrap_text(lines: &[String], width: usize) -> Vec<String> {
let mut out = Vec::new();
let mut state = ParagraphState::default();
let mut writer = ParagraphWriter::new(&mut out, width);
let mut fence_tracker = FenceTracker::default();
let link_matcher = link_reference::LinkReferenceMatcher::production();
let mut link_title_window = link_reference::LinkTitleWindow::default();
let setext_lines = crate::headings::setext_heading_lines(lines);
for (index, line) in lines.iter().enumerate() {
let blockquote = BlockquotePrefix::parse(line);
let current_depth = blockquote.map_or(0, |prefix| prefix.depth());
let inner_content = blockquote.map_or(line.as_str(), |prefix| prefix.inner());
if handle_line_preamble(
PreambleLine {
original: line,
inner: inner_content,
depth: current_depth,
is_setext: setext_lines.get(index).copied().unwrap_or(false),
},
&mut writer,
&mut state,
&mut fence_tracker,
link_matcher,
&mut link_title_window,
) {
continue;
}
let block_kind = classify_block(inner_content, link_matcher);
if dispatch_continuation(
LineContext {
original: line,
inner: inner_content,
blockquote,
block_kind,
},
&mut writer,
&mut state,
link_matcher,
&mut link_title_window,
) {
continue;
}
state.note_indent(inner_content);
let (text, hard_break) = line_break_parts(inner_content);
state.push(text, hard_break);
}
writer.flush_paragraph(&mut state);
out
}
#[cfg(test)]
mod tests;