1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
//! The P1 math fallback node and its inverse.
//!
//! P1's contract is that math is **never silently deleted**. Two shapes
//! carry an equation through the AST, and this module owns both plus the
//! conversion between them, so the format and its parser cannot drift:
//!
//! | Shape | Built by | Used where |
//! |---|---|---|
//! | `<code class="moss-math" data-moss-math="…">` in [`Inline::Other`] | [`math_inline`] | rendered HTML |
//! | `$…$` / `$$…$$` markdown source | [`math_source`] | every plain-text collector |
//!
//! **Why plain-text collectors get the source and not bare TeX.** pulldown
//! hands a walker the *inner* TeX only — delimiters stripped, inner bytes
//! otherwise untouched — so `$` + tex + `$` reproduces the author's
//! original bytes exactly. Restoring the delimiters is what keeps three
//! independent surfaces in agreement:
//!
//! 1. **The math=off slug.** `# Euler $e^{i\pi}=-1$ identity` must produce
//! the same `<h1 id>` whether or not `[site].math` is on, or flipping
//! the flag silently breaks every existing deep link, in-page TOC entry
//! and `[[Page#Heading]]` wikilink pointing at that heading.
//! 2. **The wikilink graph.** `build/scan/scan.rs` slugs headings off the
//! RAW heading line; being a line scanner it cannot know where math
//! begins, so it always includes the `$` bytes. The render side has to
//! match it, or the graph resolves a link to a fragment the page lacks.
//! 3. **`heading::extract`**, whose module doc calls byte-identity with the
//! rendered `<hN id>` "the keystone invariant".
//!
//! Bare TeX would satisfy none of the three. Markup would be actively wrong
//! in these contexts — `alt` is an HTML attribute and the slug feeds a URL
//! fragment.
use escape_text;
use Inline;
const PREFIX_INLINE: &str = r#"<code class="moss-math" data-moss-math="inline">"#;
const PREFIX_DISPLAY: &str = r#"<code class="moss-math" data-moss-math="display">"#;
const SUFFIX: &str = "</code>";
/// Build the P1 math fallback node: the equation's own markdown source —
/// delimiters included — HTML-escaped, in a marked `<code>` span.
///
/// The `data-moss-math` attribute carries display-vs-inline so a later
/// phase's renderer can typeset from the AST without re-deriving it, and so
/// the CSS can size display math differently without a second class.
///
/// **Why the `$` delimiters are kept.** P1 ships no typesetting engine, so
/// this span is what the reader actually sees. Emitting the bare inner TeX
/// would silently swallow two characters of the author's prose, which is the
/// same content-loss P1 exists to prevent — just moved from "equation
/// deleted" to "delimiters deleted". It is invisible for a real equation and
/// destructive for a false positive:
///
/// ```text
/// 一个$5,两个$10 bare TeX → 一个5,两个10 (prices corrupted)
/// source → 一个$5,两个$10 (byte-identical)
/// ```
///
/// pulldown's close rule fires on any non-whitespace byte, so unspaced CJK
/// currency parses as math (`moss doctor --math` exists to surface exactly
/// this), and `[site].math` defaults on — the false positive is the case to
/// optimize for. Keeping the delimiters also makes this node agree with
/// [`math_source`], so an equation has ONE spelling across the body, image
/// alt text, heading slugs and meta descriptions instead of two.
///
/// P2/P3 replace this span with typeset SVG, at which point the delimiters
/// disappear along with the fallback.
pub
/// Reconstruct an equation's markdown source — the TeX with its `$` / `$$`
/// delimiters restored — from the inner TeX pulldown hands the walker.
///
/// `pub` because plain-text collectors are not confined to this crate: the
/// email walker in `moss::infra::newsletter` builds an image `alt` attribute
/// and needs the same restored-delimiter form, or the two surfaces disagree
/// about what an equation looks like in plain text.
/// Recover the markdown source of a math node from an [`Inline::Other`]
/// payload, or `None` if the payload is some other raw-HTML passthrough.
///
/// The inverse of [`math_inline`]. Plain-text walkers that see the AST
/// rather than the event stream (`heading::text::inlines_to_text`, the
/// crate's one AST walker — `extract_hero` delegates to it) have no access
/// to the original event, so recovering from the node is the only way to
/// honor P1's never-silently-delete contract. Round-tripping is pinned by
/// `math_source_round_trips_through_the_node` below — that test is what
/// keeps this from drifting away from the builder three lines above it.
pub
/// Decode a math [`Inline::Other`] node into `(inner_tex, display)` — the raw
/// LaTeX the author typed (delimiters stripped, unescaped) and whether it was
/// `$$…$$` (display) or `$…$` (inline), or `None` for any non-math passthrough.
///
/// This is what lets the renderer route a math node through
/// [`RenderHooks::render_math`](crate::ast::RenderHooks::render_math) without a
/// dedicated `Inline::Math` AST variant (ADR-030 D3): the P1 node already
/// carries the source verbatim, so P2's typesetter recovers the exact bytes the
/// engine needs. Round-tripping `math_inline` → this is pinned by
/// `node_parts_round_trips` below.
///
/// `pub` for the same reason as [`math_source`]: the email HTML renderer in
/// `moss::infra::newsletter` walks `Document` directly (ADR-036) rather than
/// going through `RenderHooks`, so it decodes `Inline::Other` math nodes here
/// to route them through its own hosted-PNG math path, falling back to the
/// escaped source on refusal — the same three-question gate `render_math`
/// documents, just applied by a second, email-specific lowering.