lib/termtex/src/render.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const alloc_arena = @import("alloc_arena");
3 const flat = @import("flat.zig");
4 const image = @import("image.zig");
5 const layout = @import("layout.zig");
6 const parse = @import("parse.zig");
7
8 pub const Options = struct {};
9
10 pub const Rendered = struct {
11 arena: alloc_arena.Arena,
12 rows: []const []const u8,
13 width: usize,
14 height: usize,
15 baseline: usize,
16
17 pub fn deinit(self: *Rendered) void {
18 self.arena.deinit();
19 self.* = undefined;
20 }
21
22 pub fn toOwnedString(self: *const Rendered, allocator: std.mem.Allocator) std.mem.Allocator.Error![]u8 {
23 var out: std.ArrayListUnmanaged(u8) = .empty;
24 errdefer out.deinit(allocator);
25 for (self.rows, 0..) |row, index| {
26 if (index != 0) try out.append(allocator, '\n');
27 try out.appendSlice(allocator, row);
28 }
29 return out.toOwnedSlice(allocator);
30 }
31 };
32
33 pub fn render(allocator: std.mem.Allocator, source: []const u8, options: Options) !Rendered {
34 _ = options;
35 var arena = alloc_arena.Arena.init(allocator);
36 errdefer arena.deinit();
37 const expr = try parse.parse(arena.allocator(), source);
38 const box = try layout.layout(arena.allocator(), expr);
39 const rendered_rows = try layout.rows(arena.allocator(), box);
40 return .{
41 .arena = arena,
42 .rows = rendered_rows,
43 .width = box.width,
44 .height = box.height,
45 .baseline = box.baseline,
46 };
47 }
48
49 pub fn renderAlloc(allocator: std.mem.Allocator, source: []const u8, options: Options) ![]u8 {
50 var rendered = try render(allocator, source, options);
51 defer rendered.deinit();
52 return rendered.toOwnedString(allocator);
53 }
54
55 pub fn renderInlineAlloc(allocator: std.mem.Allocator, source: []const u8, options: Options) ![]u8 {
56 _ = options;
57 return flat.renderAlloc(allocator, source);
58 }
59
60 pub fn renderImage(allocator: std.mem.Allocator, source: []const u8, options: image.Options) !image.Image {
61 return image.render(allocator, source, options);
62 }
63
64 test "render scripts as baseline-aware rows" {
65 const rendered = try renderAlloc(std.testing.allocator, "x_i^2", .{});
66 defer std.testing.allocator.free(rendered);
67 try std.testing.expectEqualStrings(
68 \\ 2
69 \\x
70 \\ i
71 , rendered);
72 }
73
74 test "render fractions as text cells" {
75 const rendered = try renderAlloc(std.testing.allocator, "\\frac{a+b}{c}", .{});
76 defer std.testing.allocator.free(rendered);
77 try std.testing.expectEqualStrings(
78 \\ a+b
79 \\─────
80 \\ c
81 , rendered);
82 }
83
84 test "render large operator limits vertically" {
85 const rendered = try renderAlloc(std.testing.allocator, "\\sum_{i=0}^{n}", .{});
86 defer std.testing.allocator.free(rendered);
87 try std.testing.expectEqualStrings(
88 \\ n
89 \\ ∑
90 \\i=0
91 , rendered);
92 }
93
94 test "render explicit limits without stealing scripts" {
95 const rendered = try renderAlloc(std.testing.allocator, "\\sum\\limits_{i=0}^{n}", .{});
96 defer std.testing.allocator.free(rendered);
97 try std.testing.expectEqualStrings(
98 \\ n
99 \\ ∑
100 \\i=0
101 , rendered);
102 }
103
104 test "render explicit nolimits as side scripts" {
105 const rendered = try renderAlloc(std.testing.allocator, "\\sum\\nolimits_{i=0}", .{});
106 defer std.testing.allocator.free(rendered);
107 try std.testing.expectEqualStrings(
108 \\∑
109 \\ i=0
110 , rendered);
111 }
112
113 test "render operatorname star limits vertically" {
114 const rendered = try renderAlloc(std.testing.allocator, "\\operatorname*{argmin}_{x}", .{});
115 defer std.testing.allocator.free(rendered);
116 try std.testing.expectEqualStrings(
117 \\argmin
118 \\ x
119 , rendered);
120 }
121
122 test "render inline math compactly" {
123 const rendered = try renderInlineAlloc(std.testing.allocator, "\\alpha_i^2 + \\frac{x}{y}", .{});
124 defer std.testing.allocator.free(rendered);
125 try std.testing.expectEqualStrings("αᵢ²+x⁄y", rendered);
126 }
127
128 test "render inline math alphabets" {
129 const rendered = try renderInlineAlloc(std.testing.allocator, "\\mathbb{N}\\subseteq\\mathbb{R},\\mathcal{F}:\\mathbf{x}\\mapsto\\boldsymbol{\\alpha}", .{});
130 defer std.testing.allocator.free(rendered);
131 try std.testing.expectEqualStrings("ℕ⊆ℝ,ℱ:𝐱↦𝜶", rendered);
132 }
133
134 test "render inline unicode math alphabet aliases" {
135 const rendered = try renderInlineAlloc(std.testing.allocator, "\\symbfup{A}\\symsfup{B}\\symbfsfup{C}\\symbfsfit{D}\\symbbit{Ddeijx}\\symnormal{z}", .{});
136 defer std.testing.allocator.free(rendered);
137 try std.testing.expectEqualStrings("𝐀𝖡𝗖𝘿ⅅⅆⅇⅈⅉxz", rendered);
138 }
139
140 test "render inline explicit math spacing" {
141 const rendered = try renderInlineAlloc(std.testing.allocator, "A\\,B\\:C\\;D\\quad E\\qquad F\\!G\\hspace{0.5em}H\\hspace{9mu}I~J\\thinspace K", .{});
142 defer std.testing.allocator.free(rendered);
143 try std.testing.expectEqualStrings("A B C D E FG H I J K", rendered);
144 }
145
146 test "render cell math spacing widths" {
147 const rendered = try renderAlloc(std.testing.allocator, "A\\quad B\\qquad C", .{});
148 defer std.testing.allocator.free(rendered);
149 try std.testing.expectEqualStrings("A B C", rendered);
150 }
151
152 test "render text compatibility commands" {
153 const rendered = try renderInlineAlloc(std.testing.allocator, "\\textsc{E-App}\\;\\textrm{where}\\;\\texttt{fn}\\;\\textsf{A}", .{});
154 defer std.testing.allocator.free(rendered);
155 try std.testing.expectEqualStrings("ᴇ-ᴀᴘᴘ where 𝚏𝚗 𝖠", rendered);
156 }
157
158 test "render scientific and PL paper symbol idioms" {
159 const rendered = try renderInlineAlloc(std.testing.allocator, "\\Gamma\\vdash e:\\tau\\Longrightarrow e'\\in\\mathcal{V}\\cup\\{\\bot\\}\\iff e\\notin\\emptyset\\dots", .{});
160 defer std.testing.allocator.free(rendered);
161 try std.testing.expectEqualStrings("Γ⊢e:τ⟹e'∈𝒱∪{⊥}⇔e∉∅…", rendered);
162 }
163
164 test "render wrapper and invisible paper commands" {
165 const rendered = try renderInlineAlloc(std.testing.allocator, "\\DeclareMathOperator{\\dom}{dom}\\phantom{x}\\hspace*{1em}\\mathop{\\mathrm{dom}}\\limits_{x\\in X}\\mathrel{\\multimap}\\top\\notag\\label{eq:step}", .{});
166 defer std.testing.allocator.free(rendered);
167 try std.testing.expectEqualStrings("dom_(x∈X)⊸⊤", rendered);
168 }
169
170 test "render additional PL relation aliases" {
171 const rendered = try renderInlineAlloc(std.testing.allocator, "\\trianglelefteq\\;\\nRightarrow\\;A\\lessapprox B\\;C\\ggg D\\;\\circledast", .{});
172 defer std.testing.allocator.free(rendered);
173 try std.testing.expectEqualStrings("⊴ ⇏ A⪅B C⋙D ⊛", rendered);
174 }
175
176 test "operatorname star consumes limits marker" {
177 const rendered = try renderInlineAlloc(std.testing.allocator, "\\operatorname*{argmin}_{\\theta\\in\\mathbb{R}}", .{});
178 defer std.testing.allocator.free(rendered);
179 try std.testing.expectEqualStrings("argmin_(θ∈ℝ)", rendered);
180 }
181
182 test "render not relation idioms" {
183 const rendered = try renderInlineAlloc(std.testing.allocator, "x\\not\\equiv y\\land z\\not P", .{});
184 defer std.testing.allocator.free(rendered);
185 try std.testing.expectEqualStrings("x≢y∧z¬P", rendered);
186 }
187
188 test "render semantics bracket and transition idioms" {
189 const rendered = try renderInlineAlloc(std.testing.allocator, "\\llbracket e \\rrbracket_\\rho\\leadsto v'\\rightsquigarrow v\\prime", .{});
190 defer std.testing.allocator.free(rendered);
191 try std.testing.expectEqualStrings("⟦e⟧_(ρ)↝v'⇝v′", rendered);
192 }
193
194 test "render modulo paper idioms" {
195 const rendered = try renderInlineAlloc(std.testing.allocator, "\\gcd(a,b)\\equiv a\\bmod b\\pmod{2^n}+c\\mod{m}+d\\pod{k}", .{});
196 defer std.testing.allocator.free(rendered);
197 try std.testing.expectEqualStrings("gcd(a,b)≡a mod b (mod 2ⁿ)+c mod m+d (k)", rendered);
198 }
199
200 test "render equation tags beside display math" {
201 const rendered = try renderAlloc(std.testing.allocator, "\\sum_{i=0}^{n}\\tag{S}\\label{eq:sum}", .{});
202 defer std.testing.allocator.free(rendered);
203 try std.testing.expectEqualStrings(
204 \\ n
205 \\ ∑ (S)
206 \\i=0
207 , rendered);
208 }
209
210 test "render unresolved references compactly" {
211 const rendered = try renderInlineAlloc(std.testing.allocator, "x=y\\tag*{E-App}+\\eqref{eq:main}+\\ref{thm:main}+\\autoref{sec:intro}+\\Cref{lem:one}", .{});
212 defer std.testing.allocator.free(rendered);
213 try std.testing.expectEqualStrings("x=y E-App+(?)+?+?+?", rendered);
214 }
215
216 test "render PL inference rules" {
217 const rendered = try renderAlloc(std.testing.allocator, "\\inferrule*[right=\\textsc{E-App}]{\\Gamma\\vdash e\\Downarrow\\lambda x.e \\\\ \\Gamma\\vdash v\\in\\mathcal{V}}{\\Gamma\\vdash e\\;v\\Downarrow e'}", .{});
218 defer std.testing.allocator.free(rendered);
219 try std.testing.expectEqualStrings(
220 \\ Γ⊢e⇓λx.e
221 \\ Γ⊢v∈𝒱
222 \\────────── ᴇ-ᴀᴘᴘ
223 \\ Γ⊢e v⇓e'
224 , rendered);
225 }
226
227 test "render proof-style infer compactly inline" {
228 const rendered = try renderInlineAlloc(std.testing.allocator, "\\infer[Ax]{C}{A\\\\B}+\\inference{P\\\\Q}{R}", .{});
229 defer std.testing.allocator.free(rendered);
230 try std.testing.expectEqualStrings("(A;B)⁄(C) Ax+(P;Q)⁄(R)", rendered);
231 }
232
233 test "render labeled arrows vertically" {
234 const rendered = try renderAlloc(std.testing.allocator, "\\xrightarrow[\\alpha]{\\beta}", .{});
235 defer std.testing.allocator.free(rendered);
236 try std.testing.expectEqualStrings(
237 \\β
238 \\→
239 \\α
240 , rendered);
241 }
242
243 test "render labeled arrows in rows" {
244 const rendered = try renderAlloc(std.testing.allocator, "A\\xRightarrow{*}B", .{});
245 defer std.testing.allocator.free(rendered);
246 try std.testing.expectEqualStrings(
247 \\ *
248 \\A⇒B
249 , rendered);
250 }
251
252 test "render labeled arrows compactly inline" {
253 const rendered = try renderInlineAlloc(std.testing.allocator, "\\xleftarrow[\\ell]{r}+\\xmapsto{f}+\\xlongrightarrow{\\textsc{step}}", .{});
254 defer std.testing.allocator.free(rendered);
255 try std.testing.expectEqualStrings("←_(ℓ)^(r)+↦^(f)+⟶^(ꜱᴛᴇᴘ)", rendered);
256 }
257
258 test "render parenthesized matrix environment" {
259 const rendered = try renderAlloc(std.testing.allocator, "\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}", .{});
260 defer std.testing.allocator.free(rendered);
261 try std.testing.expectEqualStrings(
262 \\⎛a b⎞
263 \\⎝c d⎠
264 , rendered);
265 }
266
267 test "render cases environment with left brace" {
268 const rendered = try renderAlloc(std.testing.allocator, "\\begin{cases} x & x \\ge 0 \\\\ -x & x < 0 \\end{cases}", .{});
269 defer std.testing.allocator.free(rendered);
270 try std.testing.expectEqualStrings(
271 \\⎧x x≥0
272 \\⎩-x x<0
273 , rendered);
274 }
275
276 test "render aligned environment cells" {
277 const rendered = try renderAlloc(std.testing.allocator, "\\begin{aligned} x &= y+1 \\\\ &= z \\end{aligned}", .{});
278 defer std.testing.allocator.free(rendered);
279 try std.testing.expectEqualStrings(
280 \\x =y+1
281 \\ =z
282 , rendered);
283 }
284
285 test "render matrix environments compactly inline" {
286 const rendered = try renderInlineAlloc(std.testing.allocator, "\\begin{bmatrix} a & b \\\\ c & d \\end{bmatrix}", .{});
287 defer std.testing.allocator.free(rendered);
288 try std.testing.expectEqualStrings("[a,b;c,d]", rendered);
289 }
290
291 test "render array environment after column spec" {
292 const rendered = try renderAlloc(std.testing.allocator, "\\begin{array}{cc} a & b \\\\ c & d \\end{array}", .{});
293 defer std.testing.allocator.free(rendered);
294 try std.testing.expectEqualStrings(
295 \\a b
296 \\c d
297 , rendered);
298 }
299
300 test "render binomial coefficients" {
301 const rendered = try renderAlloc(std.testing.allocator, "\\binom{n}{k}", .{});
302 defer std.testing.allocator.free(rendered);
303 try std.testing.expectEqualStrings(
304 \\⎛ n ⎞
305 \\⎜───⎟
306 \\⎝ k ⎠
307 , rendered);
308 }
309
310 test "render primitive over fractions" {
311 const rendered = try renderAlloc(std.testing.allocator, "{a+b\\over c}", .{});
312 defer std.testing.allocator.free(rendered);
313 try std.testing.expectEqualStrings(
314 \\ a+b
315 \\─────
316 \\ c
317 , rendered);
318 }
319
320 test "render primitive choose and atop stacks" {
321 const choose = try renderAlloc(std.testing.allocator, "{n\\choose k}", .{});
322 defer std.testing.allocator.free(choose);
323 try std.testing.expectEqualStrings(
324 \\⎛n⎞
325 \\⎝k⎠
326 , choose);
327
328 const atop = try renderAlloc(std.testing.allocator, "{x\\atop y}", .{});
329 defer std.testing.allocator.free(atop);
330 try std.testing.expectEqualStrings(
331 \\x
332 \\y
333 , atop);
334 }
335
336 test "render primitive brack and brace stacks" {
337 const brack = try renderAlloc(std.testing.allocator, "{a\\brack b}", .{});
338 defer std.testing.allocator.free(brack);
339 try std.testing.expectEqualStrings(
340 \\⎡a⎤
341 \\⎣b⎦
342 , brack);
343
344 const brace = try renderAlloc(std.testing.allocator, "{x\\brace y}", .{});
345 defer std.testing.allocator.free(brace);
346 try std.testing.expectEqualStrings(
347 \\⎧x⎫
348 \\⎩y⎭
349 , brace);
350 }
351
352 test "render primitive fractions inside delimiters and inline" {
353 const display = try renderAlloc(std.testing.allocator, "\\left( a \\over b \\right)", .{});
354 defer std.testing.allocator.free(display);
355 try std.testing.expectEqualStrings(
356 \\⎛ a ⎞
357 \\⎜───⎟
358 \\⎝ b ⎠
359 , display);
360
361 const inline_math = try renderInlineAlloc(std.testing.allocator, "{n\\choose k}+{a\\over b}+{x\\atop y}", .{});
362 defer std.testing.allocator.free(inline_math);
363 try std.testing.expectEqualStrings("(n;k)+a⁄b+x;y", inline_math);
364 }
365
366 test "render left right delimiters around tall fractions" {
367 const rendered = try renderAlloc(std.testing.allocator, "\\left(\\frac{x}{y}\\right)", .{});
368 defer std.testing.allocator.free(rendered);
369 try std.testing.expectEqualStrings(
370 \\⎛ x ⎞
371 \\⎜───⎟
372 \\⎝ y ⎠
373 , rendered);
374 }
375
376 test "render left right delimiters around matrices" {
377 const rendered = try renderAlloc(std.testing.allocator, "\\left[\\begin{matrix} a & b \\\\ c & d \\end{matrix}\\right]", .{});
378 defer std.testing.allocator.free(rendered);
379 try std.testing.expectEqualStrings(
380 \\⎡a b⎤
381 \\⎣c d⎦
382 , rendered);
383 }
384
385 test "render middle and big delimiter idioms inline" {
386 const set = try renderInlineAlloc(std.testing.allocator, "\\left\\{x\\middle|x\\in A\\right\\}", .{});
387 defer std.testing.allocator.free(set);
388 try std.testing.expectEqualStrings("{x|x∈A}", set);
389
390 const sized = try renderInlineAlloc(std.testing.allocator, "\\bigl( x \\bigr]", .{});
391 defer std.testing.allocator.free(sized);
392 try std.testing.expectEqualStrings("(x]", sized);
393 }
394
395 test "render angle delimiters inline" {
396 const rendered = try renderInlineAlloc(std.testing.allocator, "\\left\\langle x \\right\\rangle", .{});
397 defer std.testing.allocator.free(rendered);
398 try std.testing.expectEqualStrings("⟨x⟩", rendered);
399 }
400
401 test "render over and under annotations" {
402 const overset = try renderAlloc(std.testing.allocator, "\\overset{*}{\\to}", .{});
403 defer std.testing.allocator.free(overset);
404 try std.testing.expectEqualStrings(
405 \\*
406 \\→
407 , overset);
408
409 const underset = try renderAlloc(std.testing.allocator, "\\underset{n\\to\\infty}{\\lim}", .{});
410 defer std.testing.allocator.free(underset);
411 try std.testing.expectEqualStrings(
412 \\lim
413 \\n→∞
414 , underset);
415 }
416
417 test "render braces with centered labels" {
418 const over = try renderAlloc(std.testing.allocator, "\\overbrace{a+b}^{n}", .{});
419 defer std.testing.allocator.free(over);
420 try std.testing.expectEqualStrings(
421 \\ n
422 \\⏞⏞⏞
423 \\a+b
424 , over);
425
426 const under = try renderAlloc(std.testing.allocator, "\\underbrace{a+b}_{n}", .{});
427 defer std.testing.allocator.free(under);
428 try std.testing.expectEqualStrings(
429 \\a+b
430 \\⏟⏟⏟
431 \\ n
432 , under);
433 }
434
435 test "render boxed and underline annotations" {
436 const boxed = try renderAlloc(std.testing.allocator, "\\boxed{x+y}", .{});
437 defer std.testing.allocator.free(boxed);
438 try std.testing.expectEqualStrings(
439 \\┌─────┐
440 \\│ x+y │
441 \\└─────┘
442 , boxed);
443
444 const underline = try renderAlloc(std.testing.allocator, "\\underline{x+y}", .{});
445 defer std.testing.allocator.free(underline);
446 try std.testing.expectEqualStrings(
447 \\x+y
448 \\───
449 , underline);
450 }
451
452 test "render additional accents" {
453 const rendered = try renderAlloc(std.testing.allocator, "\\ddot{x}+\\tilde{y}+\\check{z}+\\overleftarrow{AB}", .{});
454 defer std.testing.allocator.free(rendered);
455 try std.testing.expectEqualStrings(
456 \\¨ ~ ˇ ←
457 \\x+y+z+AB
458 , rendered);
459 }
460
461 test "render substacked limits" {
462 const rendered = try renderAlloc(std.testing.allocator, "\\sum_{\\substack{i=0\\\\j=0}}", .{});
463 defer std.testing.allocator.free(rendered);
464 try std.testing.expectEqualStrings(
465 \\ ∑
466 \\i=0
467 \\j=0
468 , rendered);
469 }
470
471 test "render annotations compactly inline" {
472 const rendered = try renderInlineAlloc(std.testing.allocator, "\\binom{n}{k}+\\overset{*}{\\to}+\\boxed{x}", .{});
473 defer std.testing.allocator.free(rendered);
474 try std.testing.expectEqualStrings("(n⁄k)+overset(*,→)+[x]", rendered);
475 }
476
477 test {
478 std.testing.refAllDecls(@This());
479 }