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 }