Documents: \( pairs stay in one paragraph and off addresses; formulas place at wraps
CI / site (push) Successful in 36s
CI / platforms (windows-latest) (push) Successful in 4m49s
CI / crate (push) Failing after 11m31s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

- Pairs pair inside one paragraph, heading or cell; links' addresses,
  images, code and <code>/<kbd> text stay literal.
- Adjacent formulas stay two; a parting space is dropped after parsing.
- Stray and escaped dollars, in or out of a formula, stay dollars.
- A formula's node spans its delimiters; its value is the written TeX.
- A formula wrapped whole draws at its own line's start.
- A failed inline formula shows its reason after the TeX.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude committed 2026-10-18 23:54:39 -04:00
1 parent e095f06689
commit a20e712e29
7 files changed
+524 -131

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
use markdown::mdast::Node;
use crate::documents::markdown::{options, span, tree, walk};
/// Each formula's TeX and span, and the text left.
fn read(source: &str) -> (Vec<(String, std::ops::Range<usize>)>, String) {
let root = tree(source);
let mut nodes = Vec::new();
walk(&root, &mut nodes);
let mut maths = Vec::new();
let mut text = String::new();
for node in nodes {
match node {
Node::InlineMath(math) => maths.push((math.value.clone(), span(node))),
Node::Text(written) => text.push_str(&written.value),
_ => {}
}
}
(maths, text)
}
fn tex(source: &str) -> Vec<String> {
read(source).0.into_iter().map(|(tex, _)| tex).collect()
}
fn untouched(source: &str) {
let plain = markdown::to_mdast(source, &options()).unwrap();
assert_eq!(tree(source), plain, "{source:?}");
}
#[test]
fn a_pair_spans_its_delimiters_and_keeps_its_tex() {
assert_eq!(
read(r"a \(x\) b"),
(vec![("x".into(), 2..7)], "a b".into())
);
assert_eq!(tex(r"\( x \)"), [" x "]);
assert_eq!(tex(r"\(x \)\(y\)"), ["x ", "y"]);
}
#[test]
fn adjacent_formulas_stay_two() {
for (source, maths) in [
(r"\(x\)\(y\)", vec!["x", "y"]),
(r"$x$\(y\)", vec!["x", "y"]),
(r"\(y\)$x$", vec!["y", "x"]),
(r"\[x\]\(y\)", vec!["x", "y"]),
] {
let (found, text) = read(source);
let found: Vec<_> = found.into_iter().map(|(tex, _)| tex).collect();
assert_eq!(found, maths, "{source:?}");
assert_eq!(text, "", "no parting space shows: {source:?}");
}
}
#[test]
fn dollars_stay_dollars() {
assert_eq!(tex(r"\(\$5\)"), [r"\$5"]);
assert_eq!(tex(r"\[a \$ b\]"), [r"a \$ b"]);
assert_eq!(tex(r"\(a$b\)"), ["a$b"]);
assert_eq!(
read(r"costs $5 and \(x\)"),
(vec![("x".into(), 13..18)], "costs $5 and ".into()),
"a stray dollar does not pair with a formula"
);
assert_eq!(read(r"\$\(x\)").1, "$", "an escaped dollar before");
assert_eq!(read(r"\(x\)\$").1, "$", "an escaped dollar after");
}
#[test]
fn addresses_keep_their_parentheses() {
let link = r"[go](https://e.test/\(x\))";
untouched(link);
untouched(r"![p](pics/\(x\).png)");
untouched("[go][a\\(x\\)]\n\n[a\\(x\\)]: /u");
untouched(r"<https://e.test/\(x\)>");
assert_eq!(tex(r"[\(x\)](https://e.test)"), ["x"], "link text is prose");
untouched(r"[a\(b](c)\)");
}
#[test]
fn code_and_tags_stay_literal() {
untouched(r"a <code>\(x\)</code> b");
untouched(r"a <kbd>\(x\)</kbd> b");
untouched(r"a <code>\(x\) b");
untouched(r"`\(x\)`");
untouched(r"\(a `b` c\)");
assert_eq!(tex(r"<b>\(x\)</b>"), ["x"], "other tags hold prose");
}
#[test]
fn a_pair_stays_in_one_block() {
untouched("- \\(a\n- b\\)");
untouched("\\(a\r\rb\\)");
untouched("\\(a\r\n\r\nb\\)");
untouched("# \\(a\nb\\)");
untouched("| \\(a | b\\) |\n| - | - |");
assert_eq!(tex("> \\(a\n> b\\)"), ["a\nb"], "a quote's lines");
}