From 58afbf212ead6a5859f6d8d209bdd4489946590f Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 20 Jul 2025 15:34:22 +0100 Subject: [PATCH 1/3] Add footnote conversion docs and helper --- docs/footnote-conversion.md | 43 +++++++++++++++++++++++++++++++++++++ src/footnotes.rs | 12 +++++++++-- tests/footnotes.rs | 12 +++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 docs/footnote-conversion.md diff --git a/docs/footnote-conversion.md b/docs/footnote-conversion.md new file mode 100644 index 00000000..6e69b1d7 --- /dev/null +++ b/docs/footnote-conversion.md @@ -0,0 +1,43 @@ +# Footnote Conversion + +`mdtablefix` can optionally convert bare numeric references into +GitHub-flavoured Markdown footnotes. The `convert_footnotes` function performs +this operation and is exposed via the higher-level `process_stream_opts` helper. + +Inline references that appear after punctuation are rewritten as footnote links. + +Before: + +```markdown +A useful tip.1 +``` + +After: + +```markdown +A useful tip.[^1] +``` + +When the final lines of a document form a numbered list, they are replaced with +footnote definitions. + +Before: + +```markdown +Text. + + 1. First note + 2. Second note +``` + +After: + +```markdown +Text. + + [^1] First note + [^2] Second note +``` + +`convert_footnotes` only processes the final contiguous list of numeric +references. diff --git a/src/footnotes.rs b/src/footnotes.rs index 790ea833..4f9ebf36 100644 --- a/src/footnotes.rs +++ b/src/footnotes.rs @@ -28,14 +28,22 @@ fn convert_inline(text: &str) -> String { .into_owned() } -fn convert_block(lines: &mut [String]) { +fn trimmed_range(lines: &[String], pred: F) -> (usize, usize) +where + F: Fn(&str) -> bool, +{ let end = lines .iter() .rposition(|l| !l.trim().is_empty()) .map_or(0, |i| i + 1); let start = (0..end) - .rfind(|&i| !FOOTNOTE_LINE_RE.is_match(lines[i].trim_end())) + .rfind(|&i| !pred(lines[i].trim_end())) .map_or(0, |i| i + 1); + (start, end) +} + +fn convert_block(lines: &mut [String]) { + let (start, end) = trimmed_range(lines, |l| FOOTNOTE_LINE_RE.is_match(l)); if start >= end || lines[start].trim_start().starts_with("[^") { return; diff --git a/tests/footnotes.rs b/tests/footnotes.rs index 4dc4e1db..8bad83ac 100644 --- a/tests/footnotes.rs +++ b/tests/footnotes.rs @@ -26,6 +26,18 @@ fn test_avoids_false_positives() { assert_eq!(convert_footnotes(&input), input); } +#[test] +fn test_ignores_numbers_in_inline_code() { + let input = lines_vec!("Look at `code 1` for details."); + assert_eq!(convert_footnotes(&input), input); +} + +#[test] +fn test_ignores_numbers_in_parentheses() { + let input = lines_vec!("Refer to equation (1) for context."); + assert_eq!(convert_footnotes(&input), input); +} + #[test] fn test_handles_punctuation_inside_bold() { let input = lines_vec!("It was **scary.**7"); From cbefa1ac044b927a2dde1ce1a1e0ea639b0621b0 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 20 Jul 2025 18:02:35 +0100 Subject: [PATCH 2/3] Extend footnote docs and tests --- docs/footnote-conversion.md | 18 +++++++++++++++++- src/footnotes.rs | 19 +++++++++++++++++++ tests/footnotes.rs | 23 +++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/docs/footnote-conversion.md b/docs/footnote-conversion.md index 6e69b1d7..0871fe76 100644 --- a/docs/footnote-conversion.md +++ b/docs/footnote-conversion.md @@ -18,6 +18,22 @@ After: A useful tip.[^1] ``` +Numbers inside inline code or parentheses are ignored. + +Before: + +```markdown +Look at `code 1` for details. +Refer to equation (1) for context. +``` + +After: + +```markdown +Look at `code 1` for details. +Refer to equation (1) for context. +``` + When the final lines of a document form a numbered list, they are replaced with footnote definitions. @@ -36,7 +52,7 @@ After: Text. [^1] First note - [^2] Second note +[^2] Second note ``` `convert_footnotes` only processes the final contiguous list of numeric diff --git a/src/footnotes.rs b/src/footnotes.rs index 4f9ebf36..17a06808 100644 --- a/src/footnotes.rs +++ b/src/footnotes.rs @@ -28,6 +28,25 @@ fn convert_inline(text: &str) -> String { .into_owned() } +/// Find the trailing block of lines that satisfy a predicate. +/// +/// The slice is scanned from the end and trailing blank lines are ignored. +/// The returned `(start, end)` pair can be used with slicing so that +/// `lines[start..end]` contains the contiguous region whose trimmed lines +/// evaluate to `true` when passed to `pred`. +/// +/// # Examples +/// +/// ```ignore +/// use mdtablefix::footnotes::trimmed_range; +/// let lines = vec![ +/// "A".to_string(), +/// "1. note".to_string(), +/// "2. more".to_string(), +/// ]; +/// let (start, end) = trimmed_range(&lines, |l| l.starts_with('1') || l.starts_with('2')); +/// assert_eq!((start, end), (1, 3)); +/// ``` fn trimmed_range(lines: &[String], pred: F) -> (usize, usize) where F: Fn(&str) -> bool, diff --git a/tests/footnotes.rs b/tests/footnotes.rs index 8bad83ac..46075512 100644 --- a/tests/footnotes.rs +++ b/tests/footnotes.rs @@ -38,6 +38,29 @@ fn test_ignores_numbers_in_parentheses() { assert_eq!(convert_footnotes(&input), input); } +#[test] +fn test_ignores_numbers_in_fenced_code_block() { + let input = lines_vec!( + "Here is a code block:", + "```", + "let x = 42; // note 1", + "```", + "Done." + ); + assert_eq!(convert_footnotes(&input), input); +} + +#[test] +fn test_ignores_numbers_in_indented_code_block() { + let input = lines_vec!( + " let a = 1;", + " let b = 2; // number 2", + "", + "Outside." + ); + assert_eq!(convert_footnotes(&input), input); +} + #[test] fn test_handles_punctuation_inside_bold() { let input = lines_vec!("It was **scary.**7"); From 63135c3d2dc22c671f3ad11a730a4c7bf777aa2c Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 20 Jul 2025 21:58:02 +0100 Subject: [PATCH 3/3] Clarify trimmed_range docs and fix footnote guide --- docs/footnote-conversion.md | 2 +- src/footnotes.rs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/footnote-conversion.md b/docs/footnote-conversion.md index 0871fe76..6b79729d 100644 --- a/docs/footnote-conversion.md +++ b/docs/footnote-conversion.md @@ -34,7 +34,7 @@ Look at `code 1` for details. Refer to equation (1) for context. ``` -When the final lines of a document form a numbered list, they are replaced with +When the final lines of a document form a numbered list they are replaced with footnote definitions. Before: diff --git a/src/footnotes.rs b/src/footnotes.rs index 17a06808..bc0deb2b 100644 --- a/src/footnotes.rs +++ b/src/footnotes.rs @@ -31,14 +31,13 @@ fn convert_inline(text: &str) -> String { /// Find the trailing block of lines that satisfy a predicate. /// /// The slice is scanned from the end and trailing blank lines are ignored. -/// The returned `(start, end)` pair can be used with slicing so that -/// `lines[start..end]` contains the contiguous region whose trimmed lines -/// evaluate to `true` when passed to `pred`. +/// The returned `(start, end)` indices delimit the contiguous region of lines +/// whose trimmed contents cause `predicate` to return `true`. Use +/// `lines[start..end]` for slicing. /// /// # Examples /// /// ```ignore -/// use mdtablefix::footnotes::trimmed_range; /// let lines = vec![ /// "A".to_string(), /// "1. note".to_string(), @@ -47,7 +46,7 @@ fn convert_inline(text: &str) -> String { /// let (start, end) = trimmed_range(&lines, |l| l.starts_with('1') || l.starts_with('2')); /// assert_eq!((start, end), (1, 3)); /// ``` -fn trimmed_range(lines: &[String], pred: F) -> (usize, usize) +fn trimmed_range(lines: &[String], predicate: F) -> (usize, usize) where F: Fn(&str) -> bool, { @@ -56,7 +55,7 @@ where .rposition(|l| !l.trim().is_empty()) .map_or(0, |i| i + 1); let start = (0..end) - .rfind(|&i| !pred(lines[i].trim_end())) + .rfind(|&i| !predicate(lines[i].trim_end())) .map_or(0, |i| i + 1); (start, end) }