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>&lt;</mo><mi>b</mi><mtext> &amp; so </mtext></mrow>",
197         out.items,
198     );
199 }