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> ... ; '*' -> &#x2217; ; '/' -> <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(&#x2211;, lower, upper) (sum-style, limits under/over) 18// \prod -> munderover(&#x220F;, lower, upper) 19// \int -> msubsup(&#x222B;, 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, "&amp;" as *u8); return 0 } 144 if c == TX_LT { tx_emit(t, "&lt;" as *u8); return 0 } 145 if c == TX_GT { tx_emit(t, "&gt;" as *u8); return 0 } 146 if c == TX_DQUOTE { tx_emit(t, "&quot;" 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>&#x03B1;</mi>" as *u8); return 1 } 185 if tx_streq(name, "beta" as *u8) == 1 { tx_emit(t, "<mi>&#x03B2;</mi>" as *u8); return 1 } 186 if tx_streq(name, "gamma" as *u8) == 1 { tx_emit(t, "<mi>&#x03B3;</mi>" as *u8); return 1 } 187 if tx_streq(name, "delta" as *u8) == 1 { tx_emit(t, "<mi>&#x03B4;</mi>" as *u8); return 1 } 188 if tx_streq(name, "epsilon" as *u8) == 1 { tx_emit(t, "<mi>&#x03B5;</mi>" as *u8); return 1 } 189 if tx_streq(name, "zeta" as *u8) == 1 { tx_emit(t, "<mi>&#x03B6;</mi>" as *u8); return 1 } 190 if tx_streq(name, "eta" as *u8) == 1 { tx_emit(t, "<mi>&#x03B7;</mi>" as *u8); return 1 } 191 if tx_streq(name, "theta" as *u8) == 1 { tx_emit(t, "<mi>&#x03B8;</mi>" as *u8); return 1 } 192 if tx_streq(name, "iota" as *u8) == 1 { tx_emit(t, "<mi>&#x03B9;</mi>" as *u8); return 1 } 193 if tx_streq(name, "kappa" as *u8) == 1 { tx_emit(t, "<mi>&#x03BA;</mi>" as *u8); return 1 } 194 if tx_streq(name, "lambda" as *u8) == 1 { tx_emit(t, "<mi>&#x03BB;</mi>" as *u8); return 1 } 195 if tx_streq(name, "mu" as *u8) == 1 { tx_emit(t, "<mi>&#x03BC;</mi>" as *u8); return 1 } 196 if tx_streq(name, "nu" as *u8) == 1 { tx_emit(t, "<mi>&#x03BD;</mi>" as *u8); return 1 } 197 if tx_streq(name, "xi" as *u8) == 1 { tx_emit(t, "<mi>&#x03BE;</mi>" as *u8); return 1 } 198 if tx_streq(name, "pi" as *u8) == 1 { tx_emit(t, "<mi>&#x03C0;</mi>" as *u8); return 1 } 199 if tx_streq(name, "rho" as *u8) == 1 { tx_emit(t, "<mi>&#x03C1;</mi>" as *u8); return 1 } 200 if tx_streq(name, "sigma" as *u8) == 1 { tx_emit(t, "<mi>&#x03C3;</mi>" as *u8); return 1 } 201 if tx_streq(name, "tau" as *u8) == 1 { tx_emit(t, "<mi>&#x03C4;</mi>" as *u8); return 1 } 202 if tx_streq(name, "phi" as *u8) == 1 { tx_emit(t, "<mi>&#x03C6;</mi>" as *u8); return 1 } 203 if tx_streq(name, "chi" as *u8) == 1 { tx_emit(t, "<mi>&#x03C7;</mi>" as *u8); return 1 } 204 if tx_streq(name, "psi" as *u8) == 1 { tx_emit(t, "<mi>&#x03C8;</mi>" as *u8); return 1 } 205 if tx_streq(name, "omega" as *u8) == 1 { tx_emit(t, "<mi>&#x03C9;</mi>" as *u8); return 1 } 206 // uppercase greek (mi) 207 if tx_streq(name, "Gamma" as *u8) == 1 { tx_emit(t, "<mi>&#x0393;</mi>" as *u8); return 1 } 208 if tx_streq(name, "Delta" as *u8) == 1 { tx_emit(t, "<mi>&#x0394;</mi>" as *u8); return 1 } 209 if tx_streq(name, "Theta" as *u8) == 1 { tx_emit(t, "<mi>&#x0398;</mi>" as *u8); return 1 } 210 if tx_streq(name, "Lambda" as *u8) == 1 { tx_emit(t, "<mi>&#x039B;</mi>" as *u8); return 1 } 211 if tx_streq(name, "Pi" as *u8) == 1 { tx_emit(t, "<mi>&#x03A0;</mi>" as *u8); return 1 } 212 if tx_streq(name, "Sigma" as *u8) == 1 { tx_emit(t, "<mi>&#x03A3;</mi>" as *u8); return 1 } 213 if tx_streq(name, "Phi" as *u8) == 1 { tx_emit(t, "<mi>&#x03A6;</mi>" as *u8); return 1 } 214 if tx_streq(name, "Psi" as *u8) == 1 { tx_emit(t, "<mi>&#x03A8;</mi>" as *u8); return 1 } 215 if tx_streq(name, "Omega" as *u8) == 1 { tx_emit(t, "<mi>&#x03A9;</mi>" as *u8); return 1 } 216 // named operators / relations (mo) 217 if tx_streq(name, "cdot" as *u8) == 1 { tx_emit(t, "<mo>&#x22C5;</mo>" as *u8); return 1 } 218 if tx_streq(name, "times" as *u8) == 1 { tx_emit(t, "<mo>&#x00D7;</mo>" as *u8); return 1 } 219 if tx_streq(name, "div" as *u8) == 1 { tx_emit(t, "<mo>&#x00F7;</mo>" as *u8); return 1 } 220 if tx_streq(name, "pm" as *u8) == 1 { tx_emit(t, "<mo>&#x00B1;</mo>" as *u8); return 1 } 221 if tx_streq(name, "le" as *u8) == 1 { tx_emit(t, "<mo>&#x2264;</mo>" as *u8); return 1 } 222 if tx_streq(name, "leq" as *u8) == 1 { tx_emit(t, "<mo>&#x2264;</mo>" as *u8); return 1 } 223 if tx_streq(name, "ge" as *u8) == 1 { tx_emit(t, "<mo>&#x2265;</mo>" as *u8); return 1 } 224 if tx_streq(name, "geq" as *u8) == 1 { tx_emit(t, "<mo>&#x2265;</mo>" as *u8); return 1 } 225 if tx_streq(name, "neq" as *u8) == 1 { tx_emit(t, "<mo>&#x2260;</mo>" as *u8); return 1 } 226 if tx_streq(name, "ne" as *u8) == 1 { tx_emit(t, "<mo>&#x2260;</mo>" as *u8); return 1 } 227 if tx_streq(name, "approx" as *u8) == 1 { tx_emit(t, "<mo>&#x2248;</mo>" as *u8); return 1 } 228 if tx_streq(name, "equiv" as *u8) == 1 { tx_emit(t, "<mo>&#x2261;</mo>" as *u8); return 1 } 229 if tx_streq(name, "to" as *u8) == 1 { tx_emit(t, "<mo>&#x2192;</mo>" as *u8); return 1 } 230 if tx_streq(name, "rightarrow" as *u8) == 1 { tx_emit(t, "<mo>&#x2192;</mo>" as *u8); return 1 } 231 if tx_streq(name, "cdots" as *u8) == 1 { tx_emit(t, "<mo>&#x22EF;</mo>" as *u8); return 1 } 232 if tx_streq(name, "ldots" as *u8) == 1 { tx_emit(t, "<mo>&#x2026;</mo>" as *u8); return 1 } 233 // named identifiers / constants (mi) 234 if tx_streq(name, "infty" as *u8) == 1 { tx_emit(t, "<mi>&#x221E;</mi>" as *u8); return 1 } 235 if tx_streq(name, "partial" as *u8) == 1 { tx_emit(t, "<mi>&#x2202;</mi>" as *u8); return 1 } 236 if tx_streq(name, "nabla" as *u8) == 1 { tx_emit(t, "<mi>&#x2207;</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, "&#x2211;" as *u8); return 1 } // 1 => munderover 243 if tx_streq(name, "prod" as *u8) == 1 { tx_setz(b, "&#x220F;" as *u8); return 1 } // 1 => munderover 244 if tx_streq(name, "int" as *u8) == 1 { tx_setz(b, "&#x222B;" 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>&#x2212;</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>&lt;</mo>" as *u8); tx_adv(t); return 0 } 569 if c == TX_GT { tx_emit(t, "<mo>&gt;</mo>" as *u8); tx_adv(t); return 0 } 570 if c == TX_STAR { tx_emit(t, "<mo>&#x2217;</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}