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, "<" as *u8) != LM_OK { return c.err } }
89 if b == 0x3E { if lm_lit(c, ">" as *u8) != LM_OK { return c.err } }
90 if b == 0x26 { if lm_lit(c, "&" as *u8) != LM_OK { return c.err } }
91 if b == 0x22 { if lm_lit(c, """ 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}