lib/zen/src/math/mathml.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const zen = @import("../root.zig");
3 const parse = @import("parse.zig");
4
5 const html = zen.html;
6 const Allocator = std.mem.Allocator;
7
8 pub fn append(out: *std.ArrayList(u8), allocator: Allocator, node: parse.Node) Allocator.Error!void {
9 switch (node) {
10 .row => |items| {
11 if (items.len == 1) return append(out, allocator, items[0]);
12 try out.appendSlice(allocator, "<mrow>");
13 for (items) |item| try append(out, allocator, item);
14 try out.appendSlice(allocator, "</mrow>");
15 },
16 .ident => |ident| {
17 if (ident.upright) {
18 try out.appendSlice(allocator, "<mi mathvariant=\"normal\">");
19 } else {
20 try out.appendSlice(allocator, "<mi>");
21 }
22 try html.appendEscaped(out, allocator, ident.text);
23 try out.appendSlice(allocator, "</mi>");
24 },
25 .number => |text| {
26 try out.appendSlice(allocator, "<mn>");
27 try html.appendEscaped(out, allocator, text);
28 try out.appendSlice(allocator, "</mn>");
29 },
30 .operator => |operator| {
31 try out.appendSlice(allocator, "<mo");
32 if (operator.stretchy) try out.appendSlice(allocator, " stretchy=\"true\"");
33 if (operator.rigid) try out.appendSlice(allocator, " stretchy=\"false\"");
34 if (operator.movable_word) try out.appendSlice(allocator, " movablelimits=\"true\"");
35 try out.append(allocator, '>');
36 try html.appendEscaped(out, allocator, operator.text);
37 try out.appendSlice(allocator, "</mo>");
38 },
39 .space => |width| {
40 try out.appendSlice(allocator, "<mspace width=\"");
41 try out.appendSlice(allocator, width);
42 try out.appendSlice(allocator, "\"/>");
43 },
44 .text => |text| {
45 try out.appendSlice(allocator, "<mtext>");
46 try html.appendEscaped(out, allocator, text);
47 try out.appendSlice(allocator, "</mtext>");
48 },
49 .frac => |frac| {
50 try out.appendSlice(allocator, "<mfrac>");
51 try append(out, allocator, frac.first.*);
52 try append(out, allocator, frac.second.*);
53 try out.appendSlice(allocator, "</mfrac>");
54 },
55 .radical => |radical| {
56 if (radical.index) |index| {
57 try out.appendSlice(allocator, "<mroot>");
58 try append(out, allocator, radical.radicand.*);
59 try append(out, allocator, index.*);
60 try out.appendSlice(allocator, "</mroot>");
61 } else {
62 try out.appendSlice(allocator, "<msqrt>");
63 try append(out, allocator, radical.radicand.*);
64 try out.appendSlice(allocator, "</msqrt>");
65 }
66 },
67 .scripts => |scripts| {
68 const tag = scriptTag(scripts);
69 try out.append(allocator, '<');
70 try out.appendSlice(allocator, tag);
71 try out.append(allocator, '>');
72 try append(out, allocator, scripts.base.*);
73 if (scripts.sub) |sub| try append(out, allocator, sub.*);
74 if (scripts.sup) |sup| try append(out, allocator, sup.*);
75 try out.appendSlice(allocator, "</");
76 try out.appendSlice(allocator, tag);
77 try out.append(allocator, '>');
78 },
79 .accent => |accent| {
80 try out.appendSlice(allocator, "<mover accent=\"true\">");
81 try append(out, allocator, accent.base.*);
82 try out.appendSlice(allocator, "<mo>");
83 try html.appendEscaped(out, allocator, accent.mark);
84 try out.appendSlice(allocator, "</mo></mover>");
85 },
86 .table => |table| try appendTable(out, allocator, table),
87 }
88 }
89
90 fn appendTable(out: *std.ArrayList(u8), allocator: Allocator, table: parse.Table) Allocator.Error!void {
91 const fenced = table.env.open != null or table.env.close != null;
92 if (fenced) try out.appendSlice(allocator, "<mrow>");
93 if (table.env.open) |text| try appendFence(out, allocator, text);
94 try out.appendSlice(allocator, "<mtable");
95 if (table.env.display_cells) try out.appendSlice(allocator, " displaystyle=\"true\"");
96 if (table.env.class) |class| {
97 try out.appendSlice(allocator, " class=\"");
98 try out.appendSlice(allocator, class);
99 try out.append(allocator, '"');
100 }
101 try out.append(allocator, '>');
102 for (table.rows) |row| {
103 try out.appendSlice(allocator, "<mtr>");
104 for (row) |cell| {
105 try out.appendSlice(allocator, "<mtd>");
106 for (cell.row) |item| try append(out, allocator, item);
107 try out.appendSlice(allocator, "</mtd>");
108 }
109 try out.appendSlice(allocator, "</mtr>");
110 }
111 try out.appendSlice(allocator, "</mtable>");
112 if (table.env.close) |text| try appendFence(out, allocator, text);
113 if (fenced) try out.appendSlice(allocator, "</mrow>");
114 }
115
116 fn appendFence(out: *std.ArrayList(u8), allocator: Allocator, text: []const u8) Allocator.Error!void {
117 try out.appendSlice(allocator, "<mo stretchy=\"true\">");
118 try html.appendEscaped(out, allocator, text);
119 try out.appendSlice(allocator, "</mo>");
120 }
121
122 fn scriptTag(scripts: parse.Scripts) []const u8 {
123 const both = scripts.sub != null and scripts.sup != null;
124 if (scripts.limits) {
125 if (both) return "munderover";
126 return if (scripts.sub != null) "munder" else "mover";
127 }
128 if (both) return "msubsup";
129 return if (scripts.sub != null) "msub" else "msup";
130 }
131
132 test "mathml emits scripts by placement preference" {
133 var parsed = try parse.parse(std.testing.allocator, "\\sum_{i}^{n} x_i", null);
134 defer parsed.deinit();
135 var out: std.ArrayList(u8) = .empty;
136 defer out.deinit(std.testing.allocator);
137 try append(&out, std.testing.allocator, parsed.root);
138 try std.testing.expectEqualStrings(
139 "<mrow><munderover><mo>∑</mo><mi>i</mi><mi>n</mi></munderover>" ++
140 "<msub><mi>x</mi><mi>i</mi></msub></mrow>",
141 out.items,
142 );
143 }
144
145 test "mathml emits fenced matrices" {
146 var parsed = try parse.parse(std.testing.allocator, "\\begin{pmatrix} a & b \\\\ & 1 \\end{pmatrix}", null);
147 defer parsed.deinit();
148 var out: std.ArrayList(u8) = .empty;
149 defer out.deinit(std.testing.allocator);
150 try append(&out, std.testing.allocator, parsed.root);
151 try std.testing.expectEqualStrings(
152 "<mrow><mo stretchy=\"true\">(</mo><mtable>" ++
153 "<mtr><mtd><mi>a</mi></mtd><mtd><mi>b</mi></mtd></mtr>" ++
154 "<mtr><mtd></mtd><mtd><mn>1</mn></mtd></mtr>" ++
155 "</mtable><mo stretchy=\"true\">)</mo></mrow>",
156 out.items,
157 );
158 }
159
160 test "mathml emits cases with a lone brace and class" {
161 var parsed = try parse.parse(std.testing.allocator, "\\begin{cases} x & y \\end{cases}", null);
162 defer parsed.deinit();
163 var out: std.ArrayList(u8) = .empty;
164 defer out.deinit(std.testing.allocator);
165 try append(&out, std.testing.allocator, parsed.root);
166 try std.testing.expectEqualStrings(
167 "<mrow><mo stretchy=\"true\">{</mo><mtable class=\"zen-cases\">" ++
168 "<mtr><mtd><mi>x</mi></mtd><mtd><mi>y</mi></mtd></mtr>" ++
169 "</mtable></mrow>",
170 out.items,
171 );
172 }
173
174 test "mathml emits aligned with display cells" {
175 var parsed = try parse.parse(std.testing.allocator, "\\begin{aligned} a &= b \\\\ &= c \\end{aligned}", null);
176 defer parsed.deinit();
177 var out: std.ArrayList(u8) = .empty;
178 defer out.deinit(std.testing.allocator);
179 try append(&out, std.testing.allocator, parsed.root);
180 try std.testing.expectEqualStrings(
181 "<mtable displaystyle=\"true\" class=\"zen-aligned\">" ++
182 "<mtr><mtd><mi>a</mi></mtd><mtd><mo>=</mo><mi>b</mi></mtd></mtr>" ++
183 "<mtr><mtd></mtd><mtd><mo>=</mo><mi>c</mi></mtd></mtr>" ++
184 "</mtable>",
185 out.items,
186 );
187 }
188
189 test "mathml escapes text content" {
190 var parsed = try parse.parse(std.testing.allocator, "a < b \\text{ & so }", null);
191 defer parsed.deinit();
192 var out: std.ArrayList(u8) = .empty;
193 defer out.deinit(std.testing.allocator);
194 try append(&out, std.testing.allocator, parsed.root);
195 try std.testing.expectEqualStrings(
196 "<mrow><mi>a</mi><mo><</mo><mi>b</mi><mtext> & so </mtext></mrow>",
197 out.items,
198 );
199 }