Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/process/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ impl ProcessBuffer {
/// lines into the output.
pub(super) fn into_out(self) -> Vec<String> { self.out }

/// Drains any buffered lines into `out`, reflowing them as a table when
/// the run was recognised as one.
///
/// When the buffer is non-empty this upholds the invariant that `buf` is
/// empty and `in_table` is `false` on return, so the next line starts a
/// fresh detection window. An empty buffer short-circuits and leaves
/// `in_table` untouched: with nothing to drain there is no run to close, so
/// flushing is a no-op that preserves ordering against
/// [`push_out`](Self::push_out) without emitting a spurious blank flush.
/// Ellipsis replacement is applied here, *before* [`reflow_table`], because
/// the substitution must reach the cell text while it is still row-shaped;
/// running it after reflow would have to re-parse the emitted table.
pub(super) fn flush(&mut self) {
debug!(
in_table = self.in_table,
Expand All @@ -88,11 +100,26 @@ impl ProcessBuffer {
self.in_table = false;
}

/// Emits `line` verbatim after first flushing any pending table.
///
/// The flush is mandatory: a verbatim line (a code fence, for instance)
/// closes whatever table run preceded it, and appending it directly to
/// `out` without flushing would let it jump ahead of buffered rows that
/// belong earlier in the document. Flushing first keeps source ordering
/// intact.
pub(super) fn push_verbatim(&mut self, line: &str) {
self.flush();
self.out.push(line.to_string());
}

/// Consumes a code-fence marker line, returning `true` when it was handled.
///
/// A fence marker can never be part of a table, so it must terminate the
/// current run; the line is emitted through [`push_verbatim`](Self::push_verbatim)
/// so the pending table flushes first and ordering is preserved. Non-marker
/// lines return `false` immediately, signalling the caller to fall through
/// to its in-fence and table-detection handling; this method deliberately
/// makes no decision about lines *inside* a fence.
pub(super) fn handle_fence_line(&mut self, line: &str, is_fence_marker: bool) -> bool {
if !is_fence_marker {
return false;
Expand All @@ -102,6 +129,19 @@ impl ProcessBuffer {
true
}

/// Routes a non-fence line through table detection, buffering it or handing
/// it back for verbatim emission.
///
/// Returns `None` when the line has been absorbed into the pending table
/// run (`buf`), and `Some(line)` when the caller should emit it after the
/// buffer has been flushed. The invariant is that `buf` only ever holds
/// genuine table rows: every path that meets a line which cannot belong to
/// the current table flushes before yielding it, so a stray row can never
/// make [`reflow_table`] bail on an otherwise valid table. The ordering of
/// the guards is load-bearing — indented code blocks and block boundaries
/// (see the inline comments) must be recognised *before* the permissive
/// pipe heuristic, which would otherwise swallow lines that merely happen
/// to contain a `|`.
pub(super) fn handle_table_line(&mut self, line: String) -> Option<String> {
// A leading indent of four or more columns marks a Markdown indented
// code block, so such a line must stay verbatim and never enter table
Expand Down
35 changes: 35 additions & 0 deletions src/wrap/continuation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@ pub(super) fn apply_continuation_chunk(
}
}

/// Splits the pending buffer at a close-then-reopen boundary, emitting the
/// resolved prefix and re-seeding `pending` with the freshly reopened span.
///
/// A single continuation chunk can close the currently open code span and open
/// a new one; this helper exists for exactly that case. It flushes everything
/// up to `split_at`, then rebuilds `pending.rest` so it holds only the reopened
/// span, prefixed with its `new_len` backtick run. Two invariants must survive
/// the rewrite: `synthetic_join_spaces` offsets are rebased onto the new,
/// shorter `rest` (dropping any that fall before the split), and
/// `open_fence_len` is re-derived from that `rest` so subsequent chunks scan
/// against the correct fence width. When the reopened opener sits at end of
/// line (`opener_at_eol`) the mode is forced to `TightCodeSpan`: `CommonMark`
/// would turn the following soft break into a space inside the span, and
/// preserving it would emit an MD038 (space-inside-code-span) violation.
/// Returns `true` when the reopened span is already closed, signalling the
/// caller to flush immediately.
fn reopen_pending_span(
writer: &mut ParagraphWriter<'_>,
pending: &mut PendingPrefix,
Expand Down Expand Up @@ -277,6 +293,17 @@ fn leading_run_needs_space(
}
}

/// Reports whether a join space must be suppressed because the open code span
/// ends on a nested, still-unclosed `(`.
///
/// Normally a soft-wrapped continuation is joined with a single space, but that
/// space is wrong when the pending text ends mid-token inside a code span — for
/// example a Rust path such as `foo((bar` split after the inner `(`. The guard
/// only fires when a span is actually open (`open_fence_len > 0`) and `existing`
/// ends with `(`, and only when the parenthesis nesting inside the span is
/// deeper than one: a single open paren is an ordinary word boundary that should
/// still take the space, whereas depth `> 1` signals a nested construct whose
/// halves must abut. See [`unclosed_parenthesis_depth`] for the depth count.
fn suppresses_join_space_after_nested_open_paren(existing: &str, open_fence_len: usize) -> bool {
if open_fence_len == 0 || !existing.ends_with('(') {
return false;
Expand All @@ -289,6 +316,14 @@ fn suppresses_join_space_after_nested_open_paren(existing: &str, open_fence_len:
unclosed_parenthesis_depth(code_tail) > 1
}

/// Counts the net depth of unclosed `(` runs left open by `text`.
///
/// The count saturates at zero on `)`, so a span containing more closers than
/// openers (or a leading `)`) never underflows the `usize` and reports depth
/// `0` rather than panicking. This tolerance is deliberate: the caller only
/// cares whether nesting is deeper than one, and code spans may legitimately
/// carry unbalanced parentheses that must not abort the join-space heuristic in
/// [`suppresses_join_space_after_nested_open_paren`].
fn unclosed_parenthesis_depth(text: &str) -> usize {
text.chars().fold(0usize, |depth, ch| match ch {
'(' => depth.saturating_add(1),
Expand Down
Loading