code wiki / _hdl_build / nx_tex.nx
nx_tex.nx source
↩ module page · 699 lines · 32547 B
1// nx_tex.nx -- SOVEREIGN LaTeX-math -> presentation MathML renderer (IMS Thrust D, rung 1).
2// The core of the "arXiv PDF->HTML / LaTeX" capability: turn a real, useful subset of LaTeX math
3// into WELL-FORMED presentation MathML (<math>...</math>, every open tag closed) with a tokenizer
4// + a small recursive-descent parser. PURE + DETERMINISTIC (no clock, no network) so it is trivially
5// gateable by exact-MathML KATs. Scratch memory via sys_mmap only.
6//
7// SUBSET COVERED (rung 1):
8// * digits / decimal numbers -> <mn>123</mn> / <mn>1.5</mn>
9// * letters / multi-letter identifiers -> <mi>x</mi> (one <mi> per letter, MathML-standard)
10// * operators + - = < > -> <mo>+</mo> ... ; '*' -> ∗ ; '/' -> <mo>/</mo>
11// * superscript a^b -> <msup>base sup</msup>
12// * subscript a_b -> <msub>base sub</msub>
13// * both a_b^c (or a^b_c) on a base -> <msubsup>base sub sup</msubsup>
14// * \frac{a}{b} -> <mfrac>a b</mfrac>
15// * \sqrt{x} -> <msqrt> x </msqrt>
16// * big operators with optional _lower ^upper limits (either order):
17// \sum -> munderover(∑, lower, upper) (sum-style, limits under/over)
18// \prod -> munderover(∏, lower, upper)
19// \int -> msubsup(∫, lower, upper) (integral-style, limits as sub/sup)
20// (one limit -> munder/mover or msub/msup ; no limits -> bare <mo>)
21// * greek \alpha..\omega and \Gamma..\Omega -> the Unicode glyph in <mi>
22// * \cdot \times \div \pm \le \ge \neq \approx \equiv \to \cdots \ldots -> proper <mo>
23// * \infty \partial \nabla -> proper <mi>
24// * grouping { ... } -> <mrow>...</mrow> (transparent)
25// * \left( ... \right) with ( ) [ ] | and \{ \} -> <mrow><mo>(</mo>...<mo>)</mo></mrow>
26//
27// Anything UNSUPPORTED (unknown \command) degrades GRACEFULLY: emitted as an html-escaped
28// <merror><mtext>\name</mtext></merror> and parsing CONTINUES -- never a crash, never malformed
29// output. tx_render writes the full <math ...>...</math>, NUL-terminates, and returns byte length.
30//
31// VERIFICATION DOCTRINE: MathML is a STANDARD (non-novel) format -> the implementation is 100%
32// sovereign here; nx_tex_gate proves the output against hand-verified expected-MathML KATs + a
33// balanced-tag well-formedness check, plus a 3rd-party structural cross-check WHEN a reference
34// (latexml/node) is runnable (otherwise flagged PENDING). No overclaiming.
35// Sovereign: imports only nx_syscalls (sys_mmap). license_tier: ORIGINAL
36import "nx_syscalls.nx"
37
38// ===== sealed status surface (codes 2840-2849) ================================
39const TX_OK: i64 = 0
40const TX_BAD_INPUT: i64 = 2840
41const TX_TOO_BIG: i64 = 2841
42
43// ===== sizing (M7 / no magic numbers) =========================================
44const TX_OUT_CAP: i64 = 65536 // max MathML bytes for one expression (>> any real formula)
45const TX_IN_CAP: i64 = 8192 // max LaTeX source length accepted
46const TX_MAX_DEPTH: i64 = 64 // recursion guard (deep nesting degrades, never loops)
47const TX_NAME_CAP: i64 = 64
48const TX_LIMB_CAP: i64 = 16384 // per-limit side buffer for big operators
49
50// ===== ASCII code points used by the tokenizer (no magic numbers) =============
51const TX_NUL: i64 = 0
52const TX_SP: i64 = 32
53const TX_DQUOTE: i64 = 34
54const TX_AMP: i64 = 38
55const TX_STAR: i64 = 42
56const TX_PLUS: i64 = 43
57const TX_MINUS: i64 = 45
58const TX_DOT: i64 = 46
59const TX_SLASH: i64 = 47
60const TX_0: i64 = 48
61const TX_9: i64 = 57
62const TX_LT: i64 = 60
63const TX_EQ: i64 = 61
64const TX_GT: i64 = 62
65const TX_UA: i64 = 65
66const TX_UZ: i64 = 90
67const TX_BSLASH: i64 = 92
68const TX_CARET: i64 = 94
69const TX_USCORE: i64 = 95
70const TX_LA: i64 = 97
71const TX_LZ: i64 = 122
72const TX_LBRACE: i64 = 123
73const TX_BAR: i64 = 124
74const TX_RBRACE: i64 = 125
75const TX_LPAREN: i64 = 40
76const TX_RPAREN: i64 = 41
77const TX_LBRACK: i64 = 91
78const TX_RBRACK: i64 = 93
79
80// =============================================================================
81// Parser state. src is the NUL-terminated LaTeX; pos is the read cursor; out is
82// the MathML scratch (caller-owned, >= cap); w is the write cursor; cap is the
83// out capacity; depth bounds recursion.
84// =============================================================================
85struct Tx {
86 src: *u8
87 pos: i64
88 n: i64
89 out: *u8
90 w: i64
91 cap: i64
92 depth: i64
93}
94
95// ---- low-level emit -----------------------------------------------------------
96func tx_emit(t: *Tx, s: *u8) -> i64 {
97 var i: i64 = 0
98 while s[i] != (0 as u8) {
99 if t.w < t.cap { t.out[t.w] = s[i]; t.w = t.w + 1 }
100 i = i + 1
101 }
102 return 0
103}
104func tx_emit_byte(t: *Tx, b: i64) -> i64 {
105 if t.w < t.cap { t.out[t.w] = b as u8; t.w = t.w + 1 }
106 return 0
107}
108
109// ---- char classification ------------------------------------------------------
110func tx_is_digit(c: i64) -> i64 { if c >= TX_0 { if c <= TX_9 { return 1 } } return 0 }
111func tx_is_lower(c: i64) -> i64 { if c >= TX_LA { if c <= TX_LZ { return 1 } } return 0 }
112func tx_is_upper(c: i64) -> i64 { if c >= TX_UA { if c <= TX_UZ { return 1 } } return 0 }
113func tx_is_alpha(c: i64) -> i64 {
114 if tx_is_lower(c) == 1 { return 1 }
115 if tx_is_upper(c) == 1 { return 1 }
116 return 0
117}
118
119// ---- cursor -------------------------------------------------------------------
120func tx_peek(t: *Tx) -> i64 { if t.pos >= t.n { return 0 } return t.src[t.pos] as i64 }
121func tx_adv(t: *Tx) -> i64 { t.pos = t.pos + 1; return 0 }
122func tx_skip_sp(t: *Tx) -> i64 {
123 var go: i64 = 1
124 while go == 1 { if tx_peek(t) == TX_SP { tx_adv(t) } else { go = 0 } }
125 return 0
126}
127
128// ---- string helpers -----------------------------------------------------------
129func tx_streq(a: *u8, b: *u8) -> i64 {
130 var i: i64 = 0
131 while 1 == 1 {
132 let ca: u8 = a[i]; let cb: u8 = b[i]
133 if ca != cb { return 0 }
134 if ca == (0 as u8) { return 1 }
135 i = i + 1
136 }
137 return 0
138}
139func tx_setz(dst: *u8, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[i] = s[i]; i = i + 1 } dst[i] = 0 as u8; return 0 }
140
141// ---- XML-escape a single byte into mtext/mo-safe output -----------------------
142func tx_emit_escaped(t: *Tx, c: i64) -> i64 {
143 if c == TX_AMP { tx_emit(t, "&" as *u8); return 0 }
144 if c == TX_LT { tx_emit(t, "<" as *u8); return 0 }
145 if c == TX_GT { tx_emit(t, ">" as *u8); return 0 }
146 if c == TX_DQUOTE { tx_emit(t, """ as *u8); return 0 }
147 tx_emit_byte(t, c)
148 return 0
149}
150// graceful fallback: wrap a NUL-terminated label as <merror><mtext>label</mtext></merror>.
151func tx_merror(t: *Tx, label: *u8) -> i64 {
152 tx_emit(t, "<merror><mtext>" as *u8)
153 var i: i64 = 0
154 while label[i] != (0 as u8) { tx_emit_escaped(t, label[i] as i64); i = i + 1 }
155 tx_emit(t, "</mtext></merror>" as *u8)
156 return 0
157}
158
159// =============================================================================
160// Read a backslash command name (letters after '\') into buf (NUL-terminated).
161// Assumes '\' already consumed. Returns name length.
162// =============================================================================
163func tx_read_cmd(t: *Tx, buf: *u8, cap: i64) -> i64 {
164 var k: i64 = 0
165 var go: i64 = 1
166 while go == 1 {
167 let c: i64 = tx_peek(t)
168 if tx_is_alpha(c) == 1 {
169 if k < cap - 1 { buf[k] = c as u8; k = k + 1 }
170 tx_adv(t)
171 } else { go = 0 }
172 }
173 buf[k] = 0 as u8
174 return k
175}
176
177// =============================================================================
178// Greek + named symbol table. Emit the MathML element for a command name (no
179// backslash). Returns 1 if recognized+emitted, 0 if unknown. Numeric character
180// references (&#xNNNN;) keep the output ASCII-portable.
181// =============================================================================
182func tx_emit_named(t: *Tx, name: *u8) -> i64 {
183 // lowercase greek (mi)
184 if tx_streq(name, "alpha" as *u8) == 1 { tx_emit(t, "<mi>α</mi>" as *u8); return 1 }
185 if tx_streq(name, "beta" as *u8) == 1 { tx_emit(t, "<mi>β</mi>" as *u8); return 1 }
186 if tx_streq(name, "gamma" as *u8) == 1 { tx_emit(t, "<mi>γ</mi>" as *u8); return 1 }
187 if tx_streq(name, "delta" as *u8) == 1 { tx_emit(t, "<mi>δ</mi>" as *u8); return 1 }
188 if tx_streq(name, "epsilon" as *u8) == 1 { tx_emit(t, "<mi>ε</mi>" as *u8); return 1 }
189 if tx_streq(name, "zeta" as *u8) == 1 { tx_emit(t, "<mi>ζ</mi>" as *u8); return 1 }
190 if tx_streq(name, "eta" as *u8) == 1 { tx_emit(t, "<mi>η</mi>" as *u8); return 1 }
191 if tx_streq(name, "theta" as *u8) == 1 { tx_emit(t, "<mi>θ</mi>" as *u8); return 1 }
192 if tx_streq(name, "iota" as *u8) == 1 { tx_emit(t, "<mi>ι</mi>" as *u8); return 1 }
193 if tx_streq(name, "kappa" as *u8) == 1 { tx_emit(t, "<mi>κ</mi>" as *u8); return 1 }
194 if tx_streq(name, "lambda" as *u8) == 1 { tx_emit(t, "<mi>λ</mi>" as *u8); return 1 }
195 if tx_streq(name, "mu" as *u8) == 1 { tx_emit(t, "<mi>μ</mi>" as *u8); return 1 }
196 if tx_streq(name, "nu" as *u8) == 1 { tx_emit(t, "<mi>ν</mi>" as *u8); return 1 }
197 if tx_streq(name, "xi" as *u8) == 1 { tx_emit(t, "<mi>ξ</mi>" as *u8); return 1 }
198 if tx_streq(name, "pi" as *u8) == 1 { tx_emit(t, "<mi>π</mi>" as *u8); return 1 }
199 if tx_streq(name, "rho" as *u8) == 1 { tx_emit(t, "<mi>ρ</mi>" as *u8); return 1 }
200 if tx_streq(name, "sigma" as *u8) == 1 { tx_emit(t, "<mi>σ</mi>" as *u8); return 1 }
201 if tx_streq(name, "tau" as *u8) == 1 { tx_emit(t, "<mi>τ</mi>" as *u8); return 1 }
202 if tx_streq(name, "phi" as *u8) == 1 { tx_emit(t, "<mi>φ</mi>" as *u8); return 1 }
203 if tx_streq(name, "chi" as *u8) == 1 { tx_emit(t, "<mi>χ</mi>" as *u8); return 1 }
204 if tx_streq(name, "psi" as *u8) == 1 { tx_emit(t, "<mi>ψ</mi>" as *u8); return 1 }
205 if tx_streq(name, "omega" as *u8) == 1 { tx_emit(t, "<mi>ω</mi>" as *u8); return 1 }
206 // uppercase greek (mi)
207 if tx_streq(name, "Gamma" as *u8) == 1 { tx_emit(t, "<mi>Γ</mi>" as *u8); return 1 }
208 if tx_streq(name, "Delta" as *u8) == 1 { tx_emit(t, "<mi>Δ</mi>" as *u8); return 1 }
209 if tx_streq(name, "Theta" as *u8) == 1 { tx_emit(t, "<mi>Θ</mi>" as *u8); return 1 }
210 if tx_streq(name, "Lambda" as *u8) == 1 { tx_emit(t, "<mi>Λ</mi>" as *u8); return 1 }
211 if tx_streq(name, "Pi" as *u8) == 1 { tx_emit(t, "<mi>Π</mi>" as *u8); return 1 }
212 if tx_streq(name, "Sigma" as *u8) == 1 { tx_emit(t, "<mi>Σ</mi>" as *u8); return 1 }
213 if tx_streq(name, "Phi" as *u8) == 1 { tx_emit(t, "<mi>Φ</mi>" as *u8); return 1 }
214 if tx_streq(name, "Psi" as *u8) == 1 { tx_emit(t, "<mi>Ψ</mi>" as *u8); return 1 }
215 if tx_streq(name, "Omega" as *u8) == 1 { tx_emit(t, "<mi>Ω</mi>" as *u8); return 1 }
216 // named operators / relations (mo)
217 if tx_streq(name, "cdot" as *u8) == 1 { tx_emit(t, "<mo>⋅</mo>" as *u8); return 1 }
218 if tx_streq(name, "times" as *u8) == 1 { tx_emit(t, "<mo>×</mo>" as *u8); return 1 }
219 if tx_streq(name, "div" as *u8) == 1 { tx_emit(t, "<mo>÷</mo>" as *u8); return 1 }
220 if tx_streq(name, "pm" as *u8) == 1 { tx_emit(t, "<mo>±</mo>" as *u8); return 1 }
221 if tx_streq(name, "le" as *u8) == 1 { tx_emit(t, "<mo>≤</mo>" as *u8); return 1 }
222 if tx_streq(name, "leq" as *u8) == 1 { tx_emit(t, "<mo>≤</mo>" as *u8); return 1 }
223 if tx_streq(name, "ge" as *u8) == 1 { tx_emit(t, "<mo>≥</mo>" as *u8); return 1 }
224 if tx_streq(name, "geq" as *u8) == 1 { tx_emit(t, "<mo>≥</mo>" as *u8); return 1 }
225 if tx_streq(name, "neq" as *u8) == 1 { tx_emit(t, "<mo>≠</mo>" as *u8); return 1 }
226 if tx_streq(name, "ne" as *u8) == 1 { tx_emit(t, "<mo>≠</mo>" as *u8); return 1 }
227 if tx_streq(name, "approx" as *u8) == 1 { tx_emit(t, "<mo>≈</mo>" as *u8); return 1 }
228 if tx_streq(name, "equiv" as *u8) == 1 { tx_emit(t, "<mo>≡</mo>" as *u8); return 1 }
229 if tx_streq(name, "to" as *u8) == 1 { tx_emit(t, "<mo>→</mo>" as *u8); return 1 }
230 if tx_streq(name, "rightarrow" as *u8) == 1 { tx_emit(t, "<mo>→</mo>" as *u8); return 1 }
231 if tx_streq(name, "cdots" as *u8) == 1 { tx_emit(t, "<mo>⋯</mo>" as *u8); return 1 }
232 if tx_streq(name, "ldots" as *u8) == 1 { tx_emit(t, "<mo>…</mo>" as *u8); return 1 }
233 // named identifiers / constants (mi)
234 if tx_streq(name, "infty" as *u8) == 1 { tx_emit(t, "<mi>∞</mi>" as *u8); return 1 }
235 if tx_streq(name, "partial" as *u8) == 1 { tx_emit(t, "<mi>∂</mi>" as *u8); return 1 }
236 if tx_streq(name, "nabla" as *u8) == 1 { tx_emit(t, "<mi>∇</mi>" as *u8); return 1 }
237 return 0
238}
239
240// big-operator glyph helpers (NUL-terminate the entity into buffer b).
241func tx_op_glyph(name: *u8, b: *u8) -> i64 {
242 if tx_streq(name, "sum" as *u8) == 1 { tx_setz(b, "∑" as *u8); return 1 } // 1 => munderover
243 if tx_streq(name, "prod" as *u8) == 1 { tx_setz(b, "∏" as *u8); return 1 } // 1 => munderover
244 if tx_streq(name, "int" as *u8) == 1 { tx_setz(b, "∫" as *u8); return 0 } // 0 => msubsup
245 return 0 - 1
246}
247
248// =============================================================================
249// tx_count_top_elems: count the TOP-LEVEL MathML elements in a NUL-terminated
250// MathML fragment by tracking tag nesting depth. An opening tag that moves depth
251// 0->1 is one top-level element; self-closing tags (none emitted here) and text
252// are ignored. Used to decide whether a group needs an <mrow> wrapper: exactly
253// one top-level element needs none (matches LaTeXML/MathJax canonicalization),
254// zero or many need the mrow to remain a single well-formed child node.
255// =============================================================================
256func tx_count_top_elems(s: *u8) -> i64 {
257 var i: i64 = 0
258 var depth: i64 = 0
259 var count: i64 = 0
260 while s[i] != (0 as u8) {
261 if s[i] == (TX_LT as u8) {
262 if s[i + 1] == (TX_SLASH as u8) {
263 depth = depth - 1
264 } else {
265 if depth == 0 { count = count + 1 }
266 depth = depth + 1
267 }
268 // advance past the tag to the byte after '>'
269 var go: i64 = 1
270 while go == 1 {
271 if s[i] == (0 as u8) { go = 0 } else {
272 if s[i] == (TX_GT as u8) { i = i + 1; go = 0 } else { i = i + 1 }
273 }
274 }
275 } else { i = i + 1 }
276 }
277 return count
278}
279
280// =============================================================================
281// tx_group: parse a brace group "{...}" (or a single bare atom) and emit it as
282// EXACTLY ONE well-formed MathML node. Renders the contents into a side buffer,
283// then wraps in <mrow> only when the contents are not already a single element
284// (so x^{2} and x^2 both yield <msup><mi>x</mi><mn>2</mn></msup>, matching the
285// reference renderers, while x^{a+b} keeps its grouping mrow). Consumes through
286// the matching '}' when present (graceful if absent).
287// =============================================================================
288func tx_group(t: *Tx) -> i64 {
289 let side: *u8 = sys_mmap(TX_LIMB_CAP)
290 let c: *Tx = sys_mmap(64) as *Tx
291 c.src = t.src; c.pos = t.pos; c.n = t.n
292 c.out = side; c.w = 0; c.cap = TX_LIMB_CAP; c.depth = t.depth + 1
293 tx_skip_sp(c)
294 if tx_peek(c) != TX_LBRACE {
295 // A non-braced argument is a SINGLE ATOM (LaTeX rule): in a_i^2 the subscript
296 // is just `i`, and the ^2 belongs to `a` (-> msubsup), not to `i`. So parse an
297 // atom here, NOT a factor (a factor would greedily swallow the following ^2).
298 tx_atom(c)
299 } else {
300 tx_adv(c) // consume '{'
301 if c.depth < TX_MAX_DEPTH { tx_expr(c) }
302 tx_skip_sp(c)
303 if tx_peek(c) == TX_RBRACE { tx_adv(c) }
304 }
305 side[c.w] = 0 as u8
306 t.pos = c.pos // propagate cursor
307
308 let nelem: i64 = tx_count_top_elems(side)
309 if nelem == 1 {
310 tx_emit(t, side) // already a single element -> no redundant mrow
311 } else {
312 tx_emit(t, "<mrow>" as *u8); tx_emit(t, side); tx_emit(t, "</mrow>" as *u8)
313 }
314 return 0
315}
316
317// render the next group/atom into a SIDE buffer (its own out) sharing the parent
318// source+cursor; copies pos in and back out so the parent cursor advances exactly
319// as if parsed inline. Returns the side buffer (NUL-terminated MathML).
320func tx_render_group_side(t: *Tx, side: *u8) -> *u8 {
321 let c: *Tx = sys_mmap(64) as *Tx
322 c.src = t.src
323 c.pos = t.pos
324 c.n = t.n
325 c.out = side
326 c.w = 0
327 c.cap = TX_LIMB_CAP
328 c.depth = t.depth
329 tx_group(c)
330 side[c.w] = 0 as u8
331 t.pos = c.pos // propagate cursor back to parent
332 return side
333}
334
335// =============================================================================
336// tx_command: handle a backslash command (the '\' already consumed). Covers
337// \frac, \sqrt, \left/\right fences, big operators (\sum/\prod/\int), and the
338// named symbol table; unknown -> <merror>.
339// =============================================================================
340func tx_command(t: *Tx) -> i64 {
341 let nm: *u8 = sys_mmap(TX_NAME_CAP)
342 let nlen: i64 = tx_read_cmd(t, nm, TX_NAME_CAP)
343 if nlen == 0 {
344 // lone backslash or escaped delimiter (\{ \} \, etc.): emit the next byte as <mo> if it
345 // is a delimiter, else skip it -- graceful, never a crash.
346 let d: i64 = tx_peek(t)
347 if d == TX_LBRACE { tx_adv(t); tx_emit(t, "<mo>{</mo>" as *u8); return 0 }
348 if d == TX_RBRACE { tx_adv(t); tx_emit(t, "<mo>}</mo>" as *u8); return 0 }
349 if d != 0 { tx_adv(t) }
350 return 0
351 }
352
353 // \frac{num}{den}
354 if tx_streq(nm, "frac" as *u8) == 1 {
355 tx_emit(t, "<mfrac>" as *u8)
356 tx_group(t)
357 tx_group(t)
358 tx_emit(t, "</mfrac>" as *u8)
359 return 0
360 }
361 // \sqrt{rad}
362 if tx_streq(nm, "sqrt" as *u8) == 1 {
363 tx_emit(t, "<msqrt>" as *u8)
364 tx_group(t)
365 tx_emit(t, "</msqrt>" as *u8)
366 return 0
367 }
368 // \left( ... \right) : emit an mrow with the open/close fences as <mo>.
369 if tx_streq(nm, "left" as *u8) == 1 {
370 tx_emit(t, "<mrow>" as *u8)
371 tx_skip_sp(t)
372 tx_emit_fence(t) // consumes the delimiter, emits <mo>delim</mo>
373 t.depth = t.depth + 1
374 if t.depth < TX_MAX_DEPTH { tx_expr_until_right(t) }
375 t.depth = t.depth - 1
376 tx_emit(t, "</mrow>" as *u8)
377 return 0
378 }
379
380 // big operators with optional _lower ^upper limits (either order).
381 let opbuf: *u8 = sys_mmap(32)
382 let kind: i64 = tx_op_glyph(nm, opbuf) // 1=munderover, 0=msubsup, -1=not a big op
383 if kind >= 0 {
384 // collect the two optional limit groups in whatever order they appear.
385 let sub_side: *u8 = sys_mmap(TX_LIMB_CAP)
386 let sup_side: *u8 = sys_mmap(TX_LIMB_CAP)
387 var have_sub: i64 = 0
388 var have_sup: i64 = 0
389 var scan: i64 = 1
390 while scan == 1 {
391 tx_skip_sp(t)
392 let c: i64 = tx_peek(t)
393 if c == TX_USCORE {
394 tx_adv(t)
395 tx_render_group_side(t, sub_side)
396 have_sub = 1
397 } else {
398 if c == TX_CARET {
399 tx_adv(t)
400 tx_render_group_side(t, sup_side)
401 have_sup = 1
402 } else { scan = 0 }
403 }
404 }
405 if have_sub == 1 {
406 if have_sup == 1 {
407 if kind == 1 { tx_emit(t, "<munderover>" as *u8) } else { tx_emit(t, "<msubsup>" as *u8) }
408 tx_emit(t, "<mo>" as *u8); tx_emit(t, opbuf); tx_emit(t, "</mo>" as *u8)
409 tx_emit(t, sub_side); tx_emit(t, sup_side)
410 if kind == 1 { tx_emit(t, "</munderover>" as *u8) } else { tx_emit(t, "</msubsup>" as *u8) }
411 } else {
412 if kind == 1 { tx_emit(t, "<munder>" as *u8) } else { tx_emit(t, "<msub>" as *u8) }
413 tx_emit(t, "<mo>" as *u8); tx_emit(t, opbuf); tx_emit(t, "</mo>" as *u8)
414 tx_emit(t, sub_side)
415 if kind == 1 { tx_emit(t, "</munder>" as *u8) } else { tx_emit(t, "</msub>" as *u8) }
416 }
417 } else {
418 if have_sup == 1 {
419 if kind == 1 { tx_emit(t, "<mover>" as *u8) } else { tx_emit(t, "<msup>" as *u8) }
420 tx_emit(t, "<mo>" as *u8); tx_emit(t, opbuf); tx_emit(t, "</mo>" as *u8)
421 tx_emit(t, sup_side)
422 if kind == 1 { tx_emit(t, "</mover>" as *u8) } else { tx_emit(t, "</msup>" as *u8) }
423 } else {
424 tx_emit(t, "<mo>" as *u8); tx_emit(t, opbuf); tx_emit(t, "</mo>" as *u8)
425 }
426 }
427 return 0
428 }
429
430 // named symbols (greek + operators + constants)
431 if tx_emit_named(t, nm) == 1 { return 0 }
432
433 // unknown command -> graceful <merror> with the raw "\name"
434 let lab: *u8 = sys_mmap(TX_NAME_CAP + 4)
435 lab[0] = TX_BSLASH as u8
436 var i: i64 = 0
437 while nm[i] != (0 as u8) { lab[1 + i] = nm[i]; i = i + 1 }
438 lab[1 + i] = 0 as u8
439 tx_merror(t, lab)
440 return 0
441}
442
443// emit one delimiter as <mo>...</mo>, consuming it. Handles ( ) [ ] | and \{ \}.
444// A '.' (null delimiter, \left.) emits nothing. Unknown -> nothing (graceful).
445func tx_emit_fence(t: *Tx) -> i64 {
446 let c: i64 = tx_peek(t)
447 if c == TX_LPAREN { tx_adv(t); tx_emit(t, "<mo>(</mo>" as *u8); return 0 }
448 if c == TX_RPAREN { tx_adv(t); tx_emit(t, "<mo>)</mo>" as *u8); return 0 }
449 if c == TX_LBRACK { tx_adv(t); tx_emit(t, "<mo>[</mo>" as *u8); return 0 }
450 if c == TX_RBRACK { tx_adv(t); tx_emit(t, "<mo>]</mo>" as *u8); return 0 }
451 if c == TX_BAR { tx_adv(t); tx_emit(t, "<mo>|</mo>" as *u8); return 0 }
452 if c == TX_DOT { tx_adv(t); return 0 } // \left. -> no fence
453 if c == TX_BSLASH { // \{ or \}
454 tx_adv(t)
455 let d: i64 = tx_peek(t)
456 if d == TX_LBRACE { tx_adv(t); tx_emit(t, "<mo>{</mo>" as *u8); return 0 }
457 if d == TX_RBRACE { tx_adv(t); tx_emit(t, "<mo>}</mo>" as *u8); return 0 }
458 return 0
459 }
460 return 0
461}
462
463// =============================================================================
464// tx_factor: one ATOM plus any trailing scripts (^ and/or _). Emits the base,
465// then if a script follows, wraps base+script(s) in msup/msub/msubsup. Because
466// the base is already emitted, we splice: we record the base's start offset,
467// then re-wrap by inserting the open tag before it. To stay single-pass and
468// well-formed without buffer surgery, we instead render the BASE into a side
469// buffer first, then emit the wrapped form. This keeps output balanced.
470// =============================================================================
471func tx_factor(t: *Tx) -> i64 {
472 // render the base atom into a side buffer (so we can wrap it if scripts follow).
473 let base: *u8 = sys_mmap(TX_LIMB_CAP)
474 let c: *Tx = sys_mmap(64) as *Tx
475 c.src = t.src; c.pos = t.pos; c.n = t.n
476 c.out = base; c.w = 0; c.cap = TX_LIMB_CAP; c.depth = t.depth
477 tx_atom(c)
478 base[c.w] = 0 as u8
479 t.pos = c.pos
480
481 // look for scripts: _ and/or ^ in either order.
482 tx_skip_sp(t)
483 var has_sub: i64 = 0
484 var has_sup: i64 = 0
485 let sub_side: *u8 = sys_mmap(TX_LIMB_CAP)
486 let sup_side: *u8 = sys_mmap(TX_LIMB_CAP)
487 var scan: i64 = 1
488 while scan == 1 {
489 let ch: i64 = tx_peek(t)
490 if ch == TX_USCORE {
491 if has_sub == 1 { scan = 0 } else {
492 tx_adv(t); tx_render_group_side(t, sub_side); has_sub = 1; tx_skip_sp(t)
493 }
494 } else {
495 if ch == TX_CARET {
496 if has_sup == 1 { scan = 0 } else {
497 tx_adv(t); tx_render_group_side(t, sup_side); has_sup = 1; tx_skip_sp(t)
498 }
499 } else { scan = 0 }
500 }
501 }
502
503 if has_sub == 1 {
504 if has_sup == 1 {
505 tx_emit(t, "<msubsup>" as *u8); tx_emit(t, base); tx_emit(t, sub_side); tx_emit(t, sup_side); tx_emit(t, "</msubsup>" as *u8)
506 } else {
507 tx_emit(t, "<msub>" as *u8); tx_emit(t, base); tx_emit(t, sub_side); tx_emit(t, "</msub>" as *u8)
508 }
509 } else {
510 if has_sup == 1 {
511 tx_emit(t, "<msup>" as *u8); tx_emit(t, base); tx_emit(t, sup_side); tx_emit(t, "</msup>" as *u8)
512 } else {
513 tx_emit(t, base)
514 }
515 }
516 return 0
517}
518
519// =============================================================================
520// tx_atom: the smallest unit -- a number run, an identifier letter, an operator,
521// a brace group, or a backslash command. Emits exactly one node (or an mrow group).
522// =============================================================================
523func tx_atom(t: *Tx) -> i64 {
524 tx_skip_sp(t)
525 let c: i64 = tx_peek(t)
526 if c == 0 { return 0 }
527
528 // number run (digits with an optional single embedded '.')
529 if tx_is_digit(c) == 1 {
530 tx_emit(t, "<mn>" as *u8)
531 var go: i64 = 1
532 var seen_dot: i64 = 0
533 while go == 1 {
534 let d: i64 = tx_peek(t)
535 if tx_is_digit(d) == 1 { tx_emit_byte(t, d); tx_adv(t) }
536 else {
537 if d == TX_DOT {
538 if seen_dot == 0 {
539 // only treat '.' as decimal if a digit follows
540 if t.pos + 1 < t.n {
541 if tx_is_digit(t.src[t.pos + 1] as i64) == 1 { seen_dot = 1; tx_emit_byte(t, d); tx_adv(t) } else { go = 0 }
542 } else { go = 0 }
543 } else { go = 0 }
544 } else { go = 0 }
545 }
546 }
547 tx_emit(t, "</mn>" as *u8)
548 return 0
549 }
550
551 // identifier letter -> one <mi> per letter (MathML convention: each variable its own mi)
552 if tx_is_alpha(c) == 1 {
553 tx_emit(t, "<mi>" as *u8); tx_emit_byte(t, c); tx_emit(t, "</mi>" as *u8)
554 tx_adv(t)
555 return 0
556 }
557
558 // brace group
559 if c == TX_LBRACE { tx_group(t); return 0 }
560
561 // backslash command
562 if c == TX_BSLASH { tx_adv(t); tx_command(t); return 0 }
563
564 // operators / relations (single ASCII)
565 if c == TX_PLUS { tx_emit(t, "<mo>+</mo>" as *u8); tx_adv(t); return 0 }
566 if c == TX_MINUS { tx_emit(t, "<mo>−</mo>" as *u8); tx_adv(t); return 0 } // proper MINUS SIGN
567 if c == TX_EQ { tx_emit(t, "<mo>=</mo>" as *u8); tx_adv(t); return 0 }
568 if c == TX_LT { tx_emit(t, "<mo><</mo>" as *u8); tx_adv(t); return 0 }
569 if c == TX_GT { tx_emit(t, "<mo>></mo>" as *u8); tx_adv(t); return 0 }
570 if c == TX_STAR { tx_emit(t, "<mo>∗</mo>" as *u8); tx_adv(t); return 0 } // ASTERISK OPERATOR
571 if c == TX_SLASH { tx_emit(t, "<mo>/</mo>" as *u8); tx_adv(t); return 0 }
572 if c == TX_LPAREN { tx_emit(t, "<mo>(</mo>" as *u8); tx_adv(t); return 0 }
573 if c == TX_RPAREN { tx_emit(t, "<mo>)</mo>" as *u8); tx_adv(t); return 0 }
574 if c == TX_LBRACK { tx_emit(t, "<mo>[</mo>" as *u8); tx_adv(t); return 0 }
575 if c == TX_RBRACK { tx_emit(t, "<mo>]</mo>" as *u8); tx_adv(t); return 0 }
576 if c == TX_BAR { tx_emit(t, "<mo>|</mo>" as *u8); tx_adv(t); return 0 }
577
578 // a stray '}' or '_'/'^' with no base, or any other byte -> consume + escape as <mo>,
579 // so we never spin and never emit malformed bytes.
580 tx_emit(t, "<mo>" as *u8); tx_emit_escaped(t, c); tx_emit(t, "</mo>" as *u8)
581 tx_adv(t)
582 return 0
583}
584
585// =============================================================================
586// tx_expr: a sequence of factors until end-of-input or a closing '}'. Stops at
587// '}' WITHOUT consuming it (the group handler consumes it). Guards against zero-
588// progress (defensive: a bad atom that fails to advance would otherwise loop).
589// =============================================================================
590func tx_expr(t: *Tx) -> i64 {
591 var go: i64 = 1
592 while go == 1 {
593 tx_skip_sp(t)
594 let c: i64 = tx_peek(t)
595 if c == 0 { go = 0 } else {
596 if c == TX_RBRACE { go = 0 } else {
597 let before: i64 = t.pos
598 tx_factor(t)
599 if t.pos == before { tx_adv(t) } // force progress (never spin)
600 }
601 }
602 }
603 return 0
604}
605
606// like tx_expr but stops at a "\right" (consuming "\right" + its delimiter) -- used
607// inside \left( ... \right). Stops at end-of-input too (graceful if \right missing).
608func tx_expr_until_right(t: *Tx) -> i64 {
609 var go: i64 = 1
610 while go == 1 {
611 tx_skip_sp(t)
612 let c: i64 = tx_peek(t)
613 if c == 0 { go = 0 } else {
614 if c == TX_RBRACE { go = 0 } else {
615 if c == TX_BSLASH {
616 // peek the command WITHOUT permanently consuming if it isn't \right
617 let save: i64 = t.pos
618 tx_adv(t)
619 let nm: *u8 = sys_mmap(TX_NAME_CAP)
620 tx_read_cmd(t, nm, TX_NAME_CAP)
621 if tx_streq(nm, "right" as *u8) == 1 {
622 tx_skip_sp(t)
623 tx_emit_fence(t) // emit the closing fence
624 go = 0
625 } else {
626 t.pos = save // not \right: rewind, parse normally
627 let before: i64 = t.pos
628 tx_factor(t)
629 if t.pos == before { tx_adv(t) }
630 }
631 } else {
632 let before2: i64 = t.pos
633 tx_factor(t)
634 if t.pos == before2 { tx_adv(t) }
635 }
636 }
637 }
638 }
639 return 0
640}
641
642// =============================================================================
643// PUBLIC: tx_render(latex, out) -> byte length (>=0) or negative TX_*.
644// Writes a complete, NUL-terminated <math ...> ... </math> document into out
645// (caller-owned, >= TX_OUT_CAP). display="block" + the MathML namespace make the
646// fragment valid both standalone and inline in HTML5.
647// =============================================================================
648func tx_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
649
650func tx_render(latex: *u8, out: *u8) -> i64 {
651 if (latex as i64) == 0 { return 0 - TX_BAD_INPUT }
652 if (out as i64) == 0 { return 0 - TX_BAD_INPUT }
653 let n: i64 = tx_slen(latex)
654 if n > TX_IN_CAP { return 0 - TX_TOO_BIG }
655
656 let t: *Tx = sys_mmap(64) as *Tx
657 t.src = latex; t.pos = 0; t.n = n
658 t.out = out; t.w = 0; t.cap = TX_OUT_CAP; t.depth = 0
659
660 tx_emit(t, "<math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\">" as *u8)
661 tx_emit(t, "<mrow>" as *u8)
662 tx_expr(t)
663 tx_emit(t, "</mrow>" as *u8)
664 tx_emit(t, "</math>" as *u8)
665 out[t.w] = 0 as u8
666 return t.w
667}
668
669// inline variant: display="inline" (for in-prose formulas). Same engine.
670func tx_render_inline(latex: *u8, out: *u8) -> i64 {
671 if (latex as i64) == 0 { return 0 - TX_BAD_INPUT }
672 if (out as i64) == 0 { return 0 - TX_BAD_INPUT }
673 let n: i64 = tx_slen(latex)
674 if n > TX_IN_CAP { return 0 - TX_TOO_BIG }
675 let t: *Tx = sys_mmap(64) as *Tx
676 t.src = latex; t.pos = 0; t.n = n
677 t.out = out; t.w = 0; t.cap = TX_OUT_CAP; t.depth = 0
678 tx_emit(t, "<math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"inline\">" as *u8)
679 tx_emit(t, "<mrow>" as *u8)
680 tx_expr(t)
681 tx_emit(t, "</mrow>" as *u8)
682 tx_emit(t, "</math>" as *u8)
683 out[t.w] = 0 as u8
684 return t.w
685}
686
687// library organ: no main (the gate + page builder import it). A bare main keeps
688// the standalone build link-clean and lets a smoke run exercise tx_render.
689// library organ: the gate + page builder import it. A bare main exercises tx_render
690// for a standalone smoke run (and keeps the standalone link clean).
691func main() -> i64 {
692 let out: *u8 = sys_mmap(TX_OUT_CAP)
693 let nbytes: i64 = tx_render("x^2 + \\frac{a}{b}" as *u8, out)
694 if nbytes < 0 { sys_write(1, "tx_render FAILED\n" as *u8, 17); sys_exit(1); return 1 }
695 sys_write(1, out, nbytes)
696 sys_write(1, "\n" as *u8, 1)
697 sys_exit(0)
698 return 0
699}