From c42321ff0ec2f58431ffcd1e4302b01e10e3f4fa Mon Sep 17 00:00:00 2001 From: atlas Date: Tue, 13 Oct 2026 16:13:50 +0200 Subject: [PATCH] review classifier: escape $ in verdict cells so the forge doesn't render them as math Forgejo's markdown renders $...$ and $$...$$ as KaTeX; a reason quoting nix `${...}` put two $ in one cell and the span between them became a KaTeX parse error. The cell escaper now backslash-escapes $ like its other markup. Rule source: the math inline parser triggers on '$' and only starts a span there (modules/markup/markdown/math/inline_parser.go, Trigger/Parse in go-gitea/gitea, which Forgejo carries); goldmark's CommonMark backslash escape consumes '\$' first, so the parser never sees it. Escaping each $ also covers '$$'. Backslashes are already escaped, so '\\$' stays literal. --- .../src/review_classifier/review.rs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/swarm-controller/src/review_classifier/review.rs b/swarm-controller/src/review_classifier/review.rs index 621537b5..799cdb2f 100644 --- a/swarm-controller/src/review_classifier/review.rs +++ b/swarm-controller/src/review_classifier/review.rs @@ -736,7 +736,7 @@ fn table(rows: &[Row]) -> String { } /// `text` as one table cell's inline text: on one line, with every character -/// that could end the cell, start markup or raw HTML escaped, and a zero-width +/// that could end the cell, start markup, math (`$`) or raw HTML escaped, and a zero-width /// space after every `@`, `#`, `!`, `://` and `www.`, so it carries no live /// mention, issue or PR reference, or link. The forge links issue and PR /// numbers and bare URLs itself, so the escapes alone don't stop those. @@ -748,7 +748,8 @@ fn cell(text: &str) -> String { } for c in word.chars() { match c { - '\\' | '|' | '`' | '*' | '_' | '~' | '[' | ']' | '(' | ')' | '<' | '>' | '&' => { + '\\' | '|' | '`' | '*' | '_' | '~' | '[' | ']' | '(' | ')' | '<' | '>' | '&' + | '$' => { out.push('\\'); out.push(c); } @@ -1484,6 +1485,29 @@ mod tests { ); } + #[test] + fn a_reason_carries_no_live_math_delimiter() { + let mut r = row(None, true, true); + r.reason = + "uses https://${hyperhiveCfg.swarm.forge.domain}/metrics and $$x$$ | y".to_owned(); + let body = table(&[r]); + let line = body.lines().nth(2).expect("a data row"); + assert!( + line.contains("\\${hyperhiveCfg") && line.contains("\\$\\$x\\$\\$"), + "{line}" + ); + let bare = line + .char_indices() + .filter(|(i, c)| *c == '$' && (*i == 0 || line.as_bytes()[i - 1] != b'\\')) + .count(); + assert_eq!(bare, 0, "{line}"); + let unescaped = line + .char_indices() + .filter(|(i, c)| *c == '|' && (*i == 0 || line.as_bytes()[i - 1] != b'\\')) + .count(); + assert_eq!(unescaped, 8, "{line}"); + } + #[test] fn a_reason_carries_no_link_or_reference() { let text = cell("see https://evil.example/x, WWW.evil.example, #1, o/r#3 and !4");