code wiki / (root) / nx_mathml_lib.nx

nx_mathml_lib.nx source

↩ module page · 482 lines · 20690 B

1// nx_mathml_lib.nx -- LATEX SUBSET -> MATHML, THE SOVEREIGN MATH TYPESETTER. 2// 3// WHY THIS EXISTS (measured 2026-09-03). The estate had already chosen MathML as its math rendering path 4// and already had the symbols, and NOTHING joined them: nx_paper_native's grammar takes ".MATH <id> 5// <mathml>" with the MathML subtree VERBATIM, so an author had to hand-write MathML; nx_unicode_math 6// carries ~50 UTF-8 math symbols but no layout. nx_capsearch over 7,266 sources (corpus_complete=1) 7// returns no LaTeX reader of any kind. The missing primitive was the CONVERTER, and it is the one every 8// math surface needs: a board note, a paper, a document in the unified portal, and the applied-math UI 9// all want to write a formula the way a mathematician writes it and have it render. 10// 11// WHY MATHML AND NOT A JAVASCRIPT TYPESETTER. MathML is rendered NATIVELY by the browser, so a page that 12// emits it carries NO third-party runtime, no script, and no network fetch -- which is the estate's 13// standing rule that the oracle stays outside and nothing foreign enters the build or run path. A page 14// using MathJax or KaTeX would import the oracle instead of building the twin. This organ therefore emits 15// markup the browser already understands, and the same bytes render in the Nishi browser. 16// 17// SCOPE, DECLARED RATHER THAN IMPLIED. This is a SUBSET of LaTeX math: numbers, identifiers, operators, 18// superscripts, subscripts, fractions, roots, grouping, the Greek letters and the operator/relation 19// symbols nx_unicode_math already carries, and upright function names. It does NOT do matrices, 20// alignment environments, macros, or text mode. ★AN UNKNOWN COMMAND IS REFUSED BY NAME AND NEVER SILENTLY 21// DROPPED: a typesetter that quietly discards what it does not understand produces a formula that is 22// wrong in a way the reader cannot see, which is worse than no formula at all. 23// 24// SAFETY. Every character that reaches the output as CONTENT goes through the shared html escaper 25// (nx_html_escape.nx). The converter never copies raw input into markup, so a formula carrying markup 26// characters cannot break out of its element -- the same escape-by-construction property the unified 27// document portal is built on. 28// 29// license_tier: ORIGINAL 30import "syscalls.nx" 31import "nx_html_escape.nx" 32import "nx_unicode_math.nx" 33 34const LM_OK: i64 = 0 35const LM_ERR_UNKNOWN_CMD: i64 = 0 - 1 // a backslash command this subset does not implement 36const LM_ERR_SHORT: i64 = 0 - 2 // the output buffer could not hold the result 37const LM_ERR_UNBALANCED: i64 = 0 - 3 // a { without its } , or a } with no { 38const LM_ERR_DEPTH: i64 = 0 - 4 // nesting deeper than LM_MAX_DEPTH 39const LM_ERR_EMPTY: i64 = 0 - 5 // nothing to typeset 40 41const LM_MAX_DEPTH: i64 = 32 42const LM_MAX_CMD: i64 = 32 // longest command name accepted, e.g. leftrightarrow 43// MathML is far more verbose than LaTeX. This factor is DERIVED, not guessed: the worst construct in this 44// subset is a fraction, where "\frac{a}{b}" (11 bytes) becomes 45// "<mfrac><mrow><mi>a</mi></mrow><mrow><mi>b</mi></mrow></mfrac>" (60 bytes), a ratio under 6, and the 46// wrapper adds a fixed head and tail. 24 leaves four times that worst case and the caller is told when it 47// binds rather than being silently truncated. 48const LM_EXPAND: i64 = 24 49const LM_HEAD_RESERVE: i64 = 128 50 51struct LmCtx { 52 src: *u8, 53 n: i64, 54 p: i64, 55 out: *u8, 56 cap: i64, 57 off: i64, 58 tmp: *u8, 59 err: i64, 60 depth: i64, 61 last: i64, 62 badoff: i64, 63 badlen: i64, 64} 65 66func lm_slen(s: *u8) -> i64 { 67 var i: i64 = 0 68 while s[i] != (0 as u8) { i = i + 1 } 69 return i 70} 71 72// Append a literal. Every append is checked, so a short buffer is a NAMED refusal rather than a truncation. 73func lm_lit(c: *LmCtx, s: *u8) -> i64 { 74 if c.err != LM_OK { return c.err } 75 let n: i64 = lm_slen(s) 76 let r: i64 = he_put(c.out, c.cap, c.off, s, n) 77 if r < 0 { c.err = LM_ERR_SHORT; return c.err } 78 c.off = r 79 return LM_OK 80} 81 82// Append ESCAPED content. Content is the only thing an author controls, so it is the only thing escaped. 83func lm_text(c: *LmCtx, s: *u8, n: i64) -> i64 { 84 if c.err != LM_OK { return c.err } 85 var i: i64 = 0 86 while i < n { 87 let b: i64 = s[i] as i64 88 if b == 0x3C { if lm_lit(c, "&lt;" as *u8) != LM_OK { return c.err } } 89 if b == 0x3E { if lm_lit(c, "&gt;" as *u8) != LM_OK { return c.err } } 90 if b == 0x26 { if lm_lit(c, "&amp;" as *u8) != LM_OK { return c.err } } 91 if b == 0x22 { if lm_lit(c, "&quot;" as *u8) != LM_OK { return c.err } } 92 var plain: i64 = 1 93 if b == 0x3C { plain = 0 } 94 if b == 0x3E { plain = 0 } 95 if b == 0x26 { plain = 0 } 96 if b == 0x22 { plain = 0 } 97 if plain == 1 { 98 if c.off >= c.cap { c.err = LM_ERR_SHORT; return c.err } 99 c.out[c.off] = s[i] 100 c.off = c.off + 1 101 } 102 i = i + 1 103 } 104 return LM_OK 105} 106 107func lm_at(c: *LmCtx) -> i64 { 108 if c.p >= c.n { return 0 } 109 return c.src[c.p] as i64 110} 111 112func lm_is_digit(b: i64) -> i64 { 113 if b >= 48 { if b <= 57 { return 1 } } 114 return 0 115} 116 117func lm_is_alpha(b: i64) -> i64 { 118 if b >= 65 { if b <= 90 { return 1 } } 119 if b >= 97 { if b <= 122 { return 1 } } 120 return 0 121} 122 123func lm_is_space(b: i64) -> i64 { 124 if b == 32 { return 1 } 125 if b == 9 { return 1 } 126 if b == 10 { return 1 } 127 if b == 13 { return 1 } 128 return 0 129} 130 131func lm_skip_space(c: *LmCtx) -> i64 { 132 while lm_is_space(lm_at(c)) == 1 { c.p = c.p + 1 } 133 return 0 134} 135 136// Compare the command name at src[a..a+len) with a literal. 137func lm_cmd_is(c: *LmCtx, a: i64, len: i64, lit: *u8) -> i64 { 138 if lm_slen(lit) != len { return 0 } 139 var i: i64 = 0 140 while i < len { 141 if c.src[a + i] != lit[i] { return 0 } 142 i = i + 1 143 } 144 return 1 145} 146 147// A command that renders as a single symbol from the incumbent symbol library. Returns the UTF-8 bytes, 148// or 0 when the name is not a symbol. COMPOSED, never re-tabulated: nx_unicode_math owns these glyphs and 149// a second table would be a duplicate ruler that drifts. 150func lm_symbol(c: *LmCtx, a: i64, len: i64) -> *u8 { 151 if lm_cmd_is(c, a, len, "alpha" as *u8) == 1 { return nx_sym_alpha() } 152 if lm_cmd_is(c, a, len, "beta" as *u8) == 1 { return nx_sym_beta() } 153 if lm_cmd_is(c, a, len, "gamma" as *u8) == 1 { return nx_sym_gamma() } 154 if lm_cmd_is(c, a, len, "delta" as *u8) == 1 { return nx_sym_delta() } 155 if lm_cmd_is(c, a, len, "epsilon" as *u8) == 1 { return nx_sym_epsilon() } 156 if lm_cmd_is(c, a, len, "theta" as *u8) == 1 { return nx_sym_theta() } 157 if lm_cmd_is(c, a, len, "lambda" as *u8) == 1 { return nx_sym_lambda() } 158 if lm_cmd_is(c, a, len, "mu" as *u8) == 1 { return nx_sym_mu() } 159 if lm_cmd_is(c, a, len, "pi" as *u8) == 1 { return nx_sym_pi() } 160 if lm_cmd_is(c, a, len, "rho" as *u8) == 1 { return nx_sym_rho() } 161 if lm_cmd_is(c, a, len, "sigma" as *u8) == 1 { return nx_sym_sigma() } 162 if lm_cmd_is(c, a, len, "phi" as *u8) == 1 { return nx_sym_phi() } 163 if lm_cmd_is(c, a, len, "omega" as *u8) == 1 { return nx_sym_omega() } 164 if lm_cmd_is(c, a, len, "eta" as *u8) == 1 { return nx_sym_eta() } 165 if lm_cmd_is(c, a, len, "zeta" as *u8) == 1 { return nx_sym_zeta() } 166 if lm_cmd_is(c, a, len, "iota" as *u8) == 1 { return nx_sym_iota() } 167 if lm_cmd_is(c, a, len, "kappa" as *u8) == 1 { return nx_sym_kappa() } 168 if lm_cmd_is(c, a, len, "nu" as *u8) == 1 { return nx_sym_nu() } 169 if lm_cmd_is(c, a, len, "xi" as *u8) == 1 { return nx_sym_xi() } 170 if lm_cmd_is(c, a, len, "tau" as *u8) == 1 { return nx_sym_tau() } 171 if lm_cmd_is(c, a, len, "upsilon" as *u8) == 1 { return nx_sym_upsilon() } 172 if lm_cmd_is(c, a, len, "chi" as *u8) == 1 { return nx_sym_chi() } 173 if lm_cmd_is(c, a, len, "psi" as *u8) == 1 { return nx_sym_psi() } 174 return 0 as *u8 175} 176 177// A blackboard-bold set name. LaTeX writes \mathbb{Z}; the incumbent symbol library already carries the 178// real double-struck glyphs, so this maps rather than inventing a font variant. 179func lm_blackboard(letter: i64) -> *u8 { 180 if letter == 82 { return nx_sym_reals() } 181 if letter == 90 { return nx_sym_integers() } 182 if letter == 67 { return nx_sym_complex() } 183 if letter == 78 { return nx_sym_naturals() } 184 if letter == 81 { return nx_sym_rationals() } 185 return 0 as *u8 186} 187 188// A command that renders as an operator glyph. Same composition rule as lm_symbol; kept separate because 189// MathML wants <mo> for these and <mi> for the identifiers above, and that distinction is what makes the 190// browser space them correctly. 191func lm_operator(c: *LmCtx, a: i64, len: i64) -> *u8 { 192 if lm_cmd_is(c, a, len, "le" as *u8) == 1 { return nx_sym_le() } 193 if lm_cmd_is(c, a, len, "leq" as *u8) == 1 { return nx_sym_le() } 194 if lm_cmd_is(c, a, len, "ge" as *u8) == 1 { return nx_sym_ge() } 195 if lm_cmd_is(c, a, len, "geq" as *u8) == 1 { return nx_sym_ge() } 196 if lm_cmd_is(c, a, len, "ne" as *u8) == 1 { return nx_sym_neq() } 197 if lm_cmd_is(c, a, len, "neq" as *u8) == 1 { return nx_sym_neq() } 198 if lm_cmd_is(c, a, len, "approx" as *u8) == 1 { return nx_sym_approx() } 199 if lm_cmd_is(c, a, len, "equiv" as *u8) == 1 { return nx_sym_equiv() } 200 if lm_cmd_is(c, a, len, "propto" as *u8) == 1 { return nx_sym_propto() } 201 if lm_cmd_is(c, a, len, "times" as *u8) == 1 { return nx_sym_times() } 202 if lm_cmd_is(c, a, len, "cdot" as *u8) == 1 { return nx_sym_dot() } 203 if lm_cmd_is(c, a, len, "pm" as *u8) == 1 { return nx_sym_plus_minus() } 204 if lm_cmd_is(c, a, len, "sum" as *u8) == 1 { return nx_sym_sum() } 205 if lm_cmd_is(c, a, len, "prod" as *u8) == 1 { return nx_sym_product() } 206 if lm_cmd_is(c, a, len, "int" as *u8) == 1 { return nx_sym_integral() } 207 if lm_cmd_is(c, a, len, "partial" as *u8) == 1 { return nx_sym_partial() } 208 if lm_cmd_is(c, a, len, "nabla" as *u8) == 1 { return nx_sym_nabla() } 209 if lm_cmd_is(c, a, len, "infty" as *u8) == 1 { return nx_sym_infinity() } 210 if lm_cmd_is(c, a, len, "to" as *u8) == 1 { return nx_sym_to() } 211 if lm_cmd_is(c, a, len, "in" as *u8) == 1 { return nx_sym_in() } 212 if lm_cmd_is(c, a, len, "forall" as *u8) == 1 { return nx_sym_forall() } 213 if lm_cmd_is(c, a, len, "exists" as *u8) == 1 { return nx_sym_exists() } 214 if lm_cmd_is(c, a, len, "notin" as *u8) == 1 { return nx_sym_notin() } 215 if lm_cmd_is(c, a, len, "subset" as *u8) == 1 { return nx_sym_subset() } 216 if lm_cmd_is(c, a, len, "cup" as *u8) == 1 { return nx_sym_union() } 217 if lm_cmd_is(c, a, len, "cap" as *u8) == 1 { return nx_sym_intersect() } 218 if lm_cmd_is(c, a, len, "emptyset" as *u8) == 1 { return nx_sym_empty() } 219 if lm_cmd_is(c, a, len, "mapsto" as *u8) == 1 { return nx_sym_mapsto() } 220 if lm_cmd_is(c, a, len, "mid" as *u8) == 1 { return "|" as *u8 } 221 if lm_cmd_is(c, a, len, "bmod" as *u8) == 1 { return "mod" as *u8 } 222 return 0 as *u8 223} 224 225// An upright multi-letter function name. LaTeX writes \sin; MathML wants <mi>sin</mi> so the browser does 226// not italicise it as three separate variables. 227func lm_funcname(c: *LmCtx, a: i64, len: i64) -> *u8 { 228 if lm_cmd_is(c, a, len, "sin" as *u8) == 1 { return "sin" as *u8 } 229 if lm_cmd_is(c, a, len, "cos" as *u8) == 1 { return "cos" as *u8 } 230 if lm_cmd_is(c, a, len, "tan" as *u8) == 1 { return "tan" as *u8 } 231 if lm_cmd_is(c, a, len, "ln" as *u8) == 1 { return "ln" as *u8 } 232 if lm_cmd_is(c, a, len, "log" as *u8) == 1 { return "log" as *u8 } 233 if lm_cmd_is(c, a, len, "exp" as *u8) == 1 { return "exp" as *u8 } 234 if lm_cmd_is(c, a, len, "min" as *u8) == 1 { return "min" as *u8 } 235 if lm_cmd_is(c, a, len, "max" as *u8) == 1 { return "max" as *u8 } 236 return 0 as *u8 237} 238 239// Forward declarations: the grammar is mutually recursive (a group holds atoms, an atom can be a group). 240func lm_group(c: *LmCtx) -> i64; 241func lm_atom(c: *LmCtx) -> i64; 242func lm_script(c: *LmCtx, open_tag: *u8, close_tag: *u8) -> i64; 243 244// Parse a sequence until the matching close brace or end of input. 245func lm_seq(c: *LmCtx, stop_at_brace: i64) -> i64 { 246 while c.err == LM_OK { 247 lm_skip_space(c) 248 let b: i64 = lm_at(c) 249 if b == 0 { return LM_OK } 250 if b == 0x7D { 251 if stop_at_brace == 1 { return LM_OK } 252 c.err = LM_ERR_UNBALANCED 253 return c.err 254 } 255 lm_atom(c) 256 } 257 return c.err 258} 259 260// A GROUP is either a braced sequence wrapped in <mrow>, or a single atom. This is exactly LaTeX's rule 261// for what a superscript or a fraction argument binds to, so \frac12 and \frac{12}{3} both behave. 262func lm_group(c: *LmCtx) -> i64 { 263 if c.err != LM_OK { return c.err } 264 c.depth = c.depth + 1 265 if c.depth > LM_MAX_DEPTH { c.err = LM_ERR_DEPTH; return c.err } 266 lm_skip_space(c) 267 if lm_at(c) == 0x7B { 268 c.p = c.p + 1 269 lm_lit(c, "<mrow>" as *u8) 270 lm_seq(c, 1) 271 if c.err == LM_OK { 272 if lm_at(c) != 0x7D { c.err = LM_ERR_UNBALANCED } else { c.p = c.p + 1 } 273 } 274 lm_lit(c, "</mrow>" as *u8) 275 } else { 276 lm_lit(c, "<mrow>" as *u8) 277 lm_atom(c) 278 lm_lit(c, "</mrow>" as *u8) 279 } 280 c.depth = c.depth - 1 281 return c.err 282} 283 284// Wrap the LAST emitted atom in a script element. LaTeX's ^ and _ are POSTFIX -- they modify what came 285// before -- so the converter rewinds over the atom it has already written, re-emits it inside the script 286// wrapper, and then parses the script argument. The rewind is why the context carries `last`. 287func lm_script(c: *LmCtx, open_tag: *u8, close_tag: *u8) -> i64 { 288 if c.err != LM_OK { return c.err } 289 if c.last < 0 { 290 // A script with nothing before it: give it an EMPTY base rather than inventing one, so the 291 // formula still renders and the author can see exactly what they wrote. 292 let before: i64 = c.off 293 lm_lit(c, "<mrow></mrow>" as *u8) 294 c.last = before 295 } 296 let len: i64 = c.off - c.last 297 var i: i64 = 0 298 while i < len { c.tmp[i] = c.out[c.last + i]; i = i + 1 } 299 c.off = c.last 300 lm_lit(c, open_tag) 301 let r: i64 = he_put(c.out, c.cap, c.off, c.tmp, len) 302 if r < 0 { c.err = LM_ERR_SHORT; return c.err } 303 c.off = r 304 c.p = c.p + 1 305 lm_group(c) 306 lm_lit(c, close_tag) 307 return c.err 308} 309 310// One atom: a number run, an identifier, an operator character, a group, or a backslash command. 311func lm_atom(c: *LmCtx) -> i64 { 312 if c.err != LM_OK { return c.err } 313 lm_skip_space(c) 314 let b: i64 = lm_at(c) 315 if b == 0 { return LM_OK } 316 317 if b == 0x5E { return lm_script(c, "<msup>" as *u8, "</msup>" as *u8) } 318 if b == 0x5F { return lm_script(c, "<msub>" as *u8, "</msub>" as *u8) } 319 320 let start: i64 = c.off 321 322 if b == 0x7B { 323 lm_group(c) 324 c.last = start 325 return c.err 326 } 327 328 if lm_is_digit(b) == 1 { 329 lm_lit(c, "<mn>" as *u8) 330 let a0: i64 = c.p 331 while lm_is_digit(lm_at(c)) == 1 { c.p = c.p + 1 } 332 if lm_at(c) == 0x2E { if lm_is_digit(c.src[c.p + 1] as i64) == 1 { c.p = c.p + 1 333 while lm_is_digit(lm_at(c)) == 1 { c.p = c.p + 1 } } } 334 lm_text(c, ((c.src as i64) + a0) as *u8, c.p - a0) 335 lm_lit(c, "</mn>" as *u8) 336 c.last = start 337 return c.err 338 } 339 340 if lm_is_alpha(b) == 1 { 341 // ONE letter per <mi>: in mathematics `ab` is a times b, not a variable named ab, and MathML 342 // spaces them correctly only when they are separate identifiers. 343 lm_lit(c, "<mi>" as *u8) 344 lm_text(c, ((c.src as i64) + c.p) as *u8, 1) 345 lm_lit(c, "</mi>" as *u8) 346 c.p = c.p + 1 347 c.last = start 348 return c.err 349 } 350 351 if b == 0x5C { 352 c.p = c.p + 1 353 let a0: i64 = c.p 354 while lm_is_alpha(lm_at(c)) == 1 { c.p = c.p + 1 } 355 let len: i64 = c.p - a0 356 if len == 0 { c.err = LM_ERR_UNKNOWN_CMD; c.badoff = a0; c.badlen = 1; return c.err } 357 if len > LM_MAX_CMD { c.err = LM_ERR_UNKNOWN_CMD; c.badoff = a0; c.badlen = len; return c.err } 358 359 if lm_cmd_is(c, a0, len, "frac" as *u8) == 1 { 360 lm_lit(c, "<mfrac>" as *u8) 361 lm_group(c) 362 lm_group(c) 363 lm_lit(c, "</mfrac>" as *u8) 364 c.last = start 365 return c.err 366 } 367 if lm_cmd_is(c, a0, len, "sqrt" as *u8) == 1 { 368 lm_lit(c, "<msqrt>" as *u8) 369 lm_group(c) 370 lm_lit(c, "</msqrt>" as *u8) 371 c.last = start 372 return c.err 373 } 374 if lm_cmd_is(c, a0, len, "left" as *u8) == 1 { return LM_OK } 375 if lm_cmd_is(c, a0, len, "right" as *u8) == 1 { return LM_OK } 376 if lm_cmd_is(c, a0, len, "mathbb" as *u8) == 1 { 377 lm_skip_space(c) 378 var letter: i64 = 0 379 if lm_at(c) == 0x7B { c.p = c.p + 1; letter = lm_at(c); c.p = c.p + 1 380 if lm_at(c) == 0x7D { c.p = c.p + 1 } else { c.err = LM_ERR_UNBALANCED; return c.err } } 381 else { letter = lm_at(c); c.p = c.p + 1 } 382 let bb: *u8 = lm_blackboard(letter) 383 if (bb as i64) == 0 { c.err = LM_ERR_UNKNOWN_CMD; c.badoff = a0; c.badlen = len; return c.err } 384 lm_lit(c, "<mi>" as *u8); lm_lit(c, bb); lm_lit(c, "</mi>" as *u8) 385 c.last = start 386 return c.err 387 } 388 389 let sym: *u8 = lm_symbol(c, a0, len) 390 if (sym as i64) != 0 { 391 lm_lit(c, "<mi>" as *u8); lm_lit(c, sym); lm_lit(c, "</mi>" as *u8) 392 c.last = start 393 return c.err 394 } 395 let op: *u8 = lm_operator(c, a0, len) 396 if (op as i64) != 0 { 397 lm_lit(c, "<mo>" as *u8); lm_lit(c, op); lm_lit(c, "</mo>" as *u8) 398 c.last = start 399 return c.err 400 } 401 let fn: *u8 = lm_funcname(c, a0, len) 402 if (fn as i64) != 0 { 403 lm_lit(c, "<mi>" as *u8); lm_lit(c, fn); lm_lit(c, "</mi>" as *u8) 404 c.last = start 405 return c.err 406 } 407 // ★REFUSED BY NAME. A typesetter that silently drops what it cannot read produces a formula that 408 // is wrong in a way the reader cannot see. 409 c.err = LM_ERR_UNKNOWN_CMD 410 c.badoff = a0 411 c.badlen = len 412 return c.err 413 } 414 415 // Any other printable byte is an operator character. Escaped, so markup cannot leak through. 416 lm_lit(c, "<mo>" as *u8) 417 lm_text(c, ((c.src as i64) + c.p) as *u8, 1) 418 lm_lit(c, "</mo>" as *u8) 419 c.p = c.p + 1 420 c.last = start 421 return c.err 422} 423 424func lm_err_name(e: i64) -> *u8 { 425 if e == LM_OK { return "OK" as *u8 } 426 if e == LM_ERR_UNKNOWN_CMD { return "UNKNOWN-COMMAND" as *u8 } 427 if e == LM_ERR_SHORT { return "OUTPUT-BUFFER-TOO-SMALL" as *u8 } 428 if e == LM_ERR_UNBALANCED { return "UNBALANCED-BRACES" as *u8 } 429 if e == LM_ERR_DEPTH { return "NESTING-TOO-DEEP" as *u8 } 430 if e == LM_ERR_EMPTY { return "EMPTY-INPUT" as *u8 } 431 return "UNKNOWN-ERROR" as *u8 432} 433 434// THE ENTRY POINT. Returns bytes written, or a negative LM_ERR_*. `display` selects block or inline. 435// On refusal the output buffer is left EMPTY rather than half-written: a partially converted formula is 436// the silent-wrongness this organ exists to prevent. 437func lm_to_mathml(latex: *u8, n: i64, out: *u8, cap: i64, display: i64, badoff: *i64, badlen: *i64) -> i64 { 438 badoff[0] = 0 - 1 439 badlen[0] = 0 440 if n <= 0 { return LM_ERR_EMPTY } 441 let need: i64 = n * LM_EXPAND + LM_HEAD_RESERVE 442 if cap < need { return LM_ERR_SHORT } 443 444 let c: *LmCtx = (sys_mmap(96)) as *LmCtx 445 c.src = latex 446 c.n = n 447 c.p = 0 448 c.out = out 449 c.cap = cap 450 c.off = 0 451 c.tmp = sys_mmap(cap) 452 c.err = LM_OK 453 c.depth = 0 454 c.last = 0 - 1 455 c.badoff = 0 - 1 456 c.badlen = 0 457 458 if display == 1 { 459 lm_lit(c, "<math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><mrow>" as *u8) 460 } else { 461 lm_lit(c, "<math xmlns=\"http://www.w3.org/1998/Math/MathML\"><mrow>" as *u8) 462 } 463 lm_seq(c, 0) 464 lm_lit(c, "</mrow></math>" as *u8) 465 466 if c.err != LM_OK { 467 badoff[0] = c.badoff 468 badlen[0] = c.badlen 469 out[0] = 0 as u8 470 sys_munmap(c.tmp, cap) 471 return c.err 472 } 473 let done: i64 = c.off 474 sys_munmap(c.tmp, cap) 475 return done 476} 477 478// The buffer a caller must provide for an input of n bytes. Published so no caller has to guess, and so 479// the factor lives in exactly one place. 480func lm_out_cap_for(n: i64) -> i64 { 481 return n * LM_EXPAND + LM_HEAD_RESERVE 482}