| @@ -0,0 +1,749 @@ |
| |
1 | // Package texmath converts a subset of TeX math to MathML. |
| |
2 | // |
| |
3 | // The subset: letters, numbers and operator characters; ^ and _; braces; |
| |
4 | // \frac and friends, \binom, \sqrt with an optional index; Greek letters and |
| |
5 | // the common symbols, relations and arrows in symbols.go; function names |
| |
6 | // (\sin, \lim, \operatorname{...}); large operators, which take their limits |
| |
7 | // above and below in display style; \left, \middle and \right; accents, |
| |
8 | // \overline, \underline and the braces; spacing commands; \text; the font |
| |
9 | // commands \mathrm, \mathbf, \mathbb, \mathcal, \mathscr, \mathfrak, \mathsf, |
| |
10 | // \mathtt, \mathit, \boldsymbol; and the environments matrix, pmatrix, |
| |
11 | // bmatrix, Bmatrix, vmatrix, Vmatrix, smallmatrix, cases, aligned, align, |
| |
12 | // gathered, gather, split, equation and displaymath. |
| |
13 | // |
| |
14 | // Anything else, including every command that defines macros, sets colors or |
| |
15 | // styles, or links, is an error; the caller shows the source instead. The |
| |
16 | // output uses only the elements and attributes listed in Elements and Attrs, |
| |
17 | // and its size is linear in the input, which is capped at MaxInput bytes and |
| |
18 | // MaxDepth levels of nesting. |
| |
19 | package texmath |
| |
20 | |
| |
21 | import ( |
| |
22 | "errors" |
| |
23 | "fmt" |
| |
24 | "html" |
| |
25 | "strings" |
| |
26 | "unicode" |
| |
27 | "unicode/utf8" |
| |
28 | ) |
| |
29 | |
| |
30 | // Limits on one expression. |
| |
31 | const ( |
| |
32 | MaxInput = 8 << 10 |
| |
33 | MaxDepth = 64 |
| |
34 | ) |
| |
35 | |
| |
36 | // Elements is every MathML element Convert emits. |
| |
37 | var Elements = []string{ |
| |
38 | "math", "mrow", "mi", "mn", "mo", "mtext", "mspace", |
| |
39 | "mfrac", "msqrt", "mroot", "msub", "msup", "msubsup", |
| |
40 | "munder", "mover", "munderover", "mtable", "mtr", "mtd", |
| |
41 | } |
| |
42 | |
| |
43 | // Attrs is every attribute Convert emits, by element, with the values it |
| |
44 | // can take: a fixed value, or "<length>" for an em length. |
| |
45 | var Attrs = map[string]map[string]string{ |
| |
46 | "math": {"display": "block"}, |
| |
47 | "mi": {"mathvariant": "normal"}, |
| |
48 | "mo": {"stretchy": "false"}, |
| |
49 | "mfrac": {"linethickness": "0"}, |
| |
50 | "mover": {"accent": "true"}, |
| |
51 | "mspace": {"width": "<length>"}, |
| |
52 | } |
| |
53 | |
| |
54 | var ( |
| |
55 | ErrTooLong = errors.New("texmath: expression too long") |
| |
56 | ErrTooDeep = errors.New("texmath: expression nested too deeply") |
| |
57 | ) |
| |
58 | |
| |
59 | // Convert renders tex as one <math> element; display selects block layout |
| |
60 | // and display-style limits. |
| |
61 | func Convert(tex string, display bool) (string, error) { |
| |
62 | if len(tex) > MaxInput { |
| |
63 | return "", ErrTooLong |
| |
64 | } |
| |
65 | if !utf8.ValidString(tex) { |
| |
66 | return "", errors.New("texmath: invalid UTF-8") |
| |
67 | } |
| |
68 | p := &parser{src: tex, display: display} |
| |
69 | kids, err := p.list(func(t token) bool { return false }) |
| |
70 | if err != nil { |
| |
71 | return "", err |
| |
72 | } |
| |
73 | if t := p.peek(); t.kind != eof { |
| |
74 | return "", p.unexpected(t) |
| |
75 | } |
| |
76 | root := &node{tag: "math", kids: kids} |
| |
77 | if display { |
| |
78 | root.attr("display", "block") |
| |
79 | } |
| |
80 | var b strings.Builder |
| |
81 | root.write(&b) |
| |
82 | return b.String(), nil |
| |
83 | } |
| |
84 | |
| |
85 | type node struct { |
| |
86 | tag string |
| |
87 | attrs [][2]string |
| |
88 | text string |
| |
89 | kids []*node |
| |
90 | limits bool // a large operator: limits go under and over in display style |
| |
91 | fn bool // a function name: a thin space follows unless a delimiter does |
| |
92 | } |
| |
93 | |
| |
94 | func (n *node) attr(k, v string) *node { |
| |
95 | n.attrs = append(n.attrs, [2]string{k, v}) |
| |
96 | return n |
| |
97 | } |
| |
98 | |
| |
99 | func (n *node) write(b *strings.Builder) { |
| |
100 | b.WriteByte('<') |
| |
101 | b.WriteString(n.tag) |
| |
102 | for _, a := range n.attrs { |
| |
103 | b.WriteString(" " + a[0] + `="` + html.EscapeString(a[1]) + `"`) |
| |
104 | } |
| |
105 | b.WriteByte('>') |
| |
106 | b.WriteString(html.EscapeString(n.text)) |
| |
107 | for _, k := range n.kids { |
| |
108 | k.write(b) |
| |
109 | } |
| |
110 | b.WriteString("</" + n.tag + ">") |
| |
111 | } |
| |
112 | |
| |
113 | func el(tag string, kids ...*node) *node { return &node{tag: tag, kids: kids} } |
| |
114 | func leaf(tag, text string) *node { return &node{tag: tag, text: text} } |
| |
115 | func row(kids []*node) *node { return &node{tag: "mrow", kids: kids} } |
| |
116 | func space(w string) *node { return (&node{tag: "mspace"}).attr("width", w) } |
| |
117 | |
| |
118 | // fixed is an operator that must not stretch to the height of its row, the |
| |
119 | // way a bracket typed without \left does not in TeX. |
| |
120 | func fixed(s string) *node { return leaf("mo", s).attr("stretchy", "false") } |
| |
121 | |
| |
122 | type kind int |
| |
123 | |
| |
124 | const ( |
| |
125 | eof kind = iota |
| |
126 | char // one character |
| |
127 | cmd // a control sequence; val is its name without the backslash |
| |
128 | ) |
| |
129 | |
| |
130 | type token struct { |
| |
131 | kind kind |
| |
132 | val string |
| |
133 | pos int |
| |
134 | } |
| |
135 | |
| |
136 | type parser struct { |
| |
137 | src string |
| |
138 | pos int |
| |
139 | depth int |
| |
140 | display bool |
| |
141 | font string |
| |
142 | } |
| |
143 | |
| |
144 | func (p *parser) skip() { |
| |
145 | for p.pos < len(p.src) { |
| |
146 | c := p.src[p.pos] |
| |
147 | switch { |
| |
148 | case c == ' ' || c == '\t' || c == '\n' || c == '\r': |
| |
149 | p.pos++ |
| |
150 | case c == '%': |
| |
151 | for p.pos < len(p.src) && p.src[p.pos] != '\n' { |
| |
152 | p.pos++ |
| |
153 | } |
| |
154 | default: |
| |
155 | return |
| |
156 | } |
| |
157 | } |
| |
158 | } |
| |
159 | |
| |
160 | func (p *parser) peek() token { |
| |
161 | save := p.pos |
| |
162 | t := p.next() |
| |
163 | p.pos = save |
| |
164 | return t |
| |
165 | } |
| |
166 | |
| |
167 | func (p *parser) next() token { |
| |
168 | p.skip() |
| |
169 | start := p.pos |
| |
170 | if p.pos >= len(p.src) { |
| |
171 | return token{kind: eof, pos: start} |
| |
172 | } |
| |
173 | if p.src[p.pos] == '\\' { |
| |
174 | p.pos++ |
| |
175 | if p.pos >= len(p.src) { |
| |
176 | return token{kind: cmd, val: "", pos: start} |
| |
177 | } |
| |
178 | if isASCIILetter(p.src[p.pos]) { |
| |
179 | end := p.pos |
| |
180 | for end < len(p.src) && isASCIILetter(p.src[end]) { |
| |
181 | end++ |
| |
182 | } |
| |
183 | name := p.src[p.pos:end] |
| |
184 | p.pos = end |
| |
185 | // \operatorname* and friends: the star is part of the name. |
| |
186 | if p.pos < len(p.src) && p.src[p.pos] == '*' && starred[name] { |
| |
187 | p.pos++ |
| |
188 | name += "*" |
| |
189 | } |
| |
190 | return token{kind: cmd, val: name, pos: start} |
| |
191 | } |
| |
192 | r, n := utf8.DecodeRuneInString(p.src[p.pos:]) |
| |
193 | p.pos += n |
| |
194 | return token{kind: cmd, val: string(r), pos: start} |
| |
195 | } |
| |
196 | r, n := utf8.DecodeRuneInString(p.src[p.pos:]) |
| |
197 | p.pos += n |
| |
198 | return token{kind: char, val: string(r), pos: start} |
| |
199 | } |
| |
200 | |
| |
201 | var starred = map[string]bool{"operatorname": true} |
| |
202 | |
| |
203 | func isASCIILetter(c byte) bool { return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' } |
| |
204 | |
| |
205 | func (p *parser) unexpected(t token) error { |
| |
206 | switch t.kind { |
| |
207 | case eof: |
| |
208 | return errors.New("texmath: unexpected end of input") |
| |
209 | case cmd: |
| |
210 | return fmt.Errorf(`texmath: unexpected \%s at %d`, t.val, t.pos) |
| |
211 | } |
| |
212 | return fmt.Errorf("texmath: unexpected %q at %d", t.val, t.pos) |
| |
213 | } |
| |
214 | |
| |
215 | func (p *parser) expect(kind kind, val string) error { |
| |
216 | if t := p.next(); t.kind != kind || t.val != val { |
| |
217 | return p.unexpected(t) |
| |
218 | } |
| |
219 | return nil |
| |
220 | } |
| |
221 | |
| |
222 | func isChar(t token, s string) bool { return t.kind == char && t.val == s } |
| |
223 | func isCmd(t token, s string) bool { return t.kind == cmd && t.val == s } |
| |
224 | |
| |
225 | // list parses atoms until the input ends or stop matches the next token, |
| |
226 | // which is left unread. |
| |
227 | func (p *parser) list(stop func(token) bool) ([]*node, error) { |
| |
228 | var out []*node |
| |
229 | for { |
| |
230 | t := p.peek() |
| |
231 | if t.kind == eof || stop(t) { |
| |
232 | return out, nil |
| |
233 | } |
| |
234 | n, err := p.scripted() |
| |
235 | if err != nil { |
| |
236 | return nil, err |
| |
237 | } |
| |
238 | if n == nil { |
| |
239 | continue |
| |
240 | } |
| |
241 | out = append(out, n) |
| |
242 | if n.fn && !p.delimiterNext() { |
| |
243 | out = append(out, space("0.1667em")) |
| |
244 | } |
| |
245 | } |
| |
246 | } |
| |
247 | |
| |
248 | // delimiterNext reports whether the next token closes a group or opens a |
| |
249 | // bracket, where TeX puts no space after a function name. |
| |
250 | func (p *parser) delimiterNext() bool { |
| |
251 | t := p.peek() |
| |
252 | if t.kind == eof { |
| |
253 | return true |
| |
254 | } |
| |
255 | if t.kind == char { |
| |
256 | return strings.Contains("()[]{}|&.,;", t.val) |
| |
257 | } |
| |
258 | switch t.val { |
| |
259 | case "left", "right", "\\", "end", ",", ";", "!", "quad", "qquad", "{", "}": |
| |
260 | return true |
| |
261 | } |
| |
262 | return false |
| |
263 | } |
| |
264 | |
| |
265 | // scripted parses one atom and any sub- and superscripts attached to it. |
| |
266 | func (p *parser) scripted() (*node, error) { |
| |
267 | var base *node |
| |
268 | if t := p.peek(); !isChar(t, "^") && !isChar(t, "_") { |
| |
269 | var err error |
| |
270 | if base, err = p.atom(); err != nil { |
| |
271 | return nil, err |
| |
272 | } |
| |
273 | } |
| |
274 | var sub, sup *node |
| |
275 | limits := base != nil && base.limits && p.display |
| |
276 | for { |
| |
277 | t := p.peek() |
| |
278 | switch { |
| |
279 | case isCmd(t, "limits"): |
| |
280 | p.next() |
| |
281 | limits = base != nil && base.limits |
| |
282 | continue |
| |
283 | case isCmd(t, "nolimits"): |
| |
284 | p.next() |
| |
285 | limits = false |
| |
286 | continue |
| |
287 | case isChar(t, "^"), isChar(t, "_"): |
| |
288 | default: |
| |
289 | if sub == nil && sup == nil { |
| |
290 | return base, nil |
| |
291 | } |
| |
292 | if base == nil { |
| |
293 | base = row(nil) |
| |
294 | } |
| |
295 | return script(base, sub, sup, limits), nil |
| |
296 | } |
| |
297 | p.next() |
| |
298 | arg, err := p.arg() |
| |
299 | if err != nil { |
| |
300 | return nil, err |
| |
301 | } |
| |
302 | if t.val == "^" { |
| |
303 | if sup != nil { |
| |
304 | return nil, fmt.Errorf("texmath: double superscript at %d", t.pos) |
| |
305 | } |
| |
306 | sup = arg |
| |
307 | } else { |
| |
308 | if sub != nil { |
| |
309 | return nil, fmt.Errorf("texmath: double subscript at %d", t.pos) |
| |
310 | } |
| |
311 | sub = arg |
| |
312 | } |
| |
313 | } |
| |
314 | } |
| |
315 | |
| |
316 | func script(base, sub, sup *node, limits bool) *node { |
| |
317 | fn := base.fn |
| |
318 | var n *node |
| |
319 | switch { |
| |
320 | case limits && sub != nil && sup != nil: |
| |
321 | n = el("munderover", base, sub, sup) |
| |
322 | case limits && sub != nil: |
| |
323 | n = el("munder", base, sub) |
| |
324 | case limits: |
| |
325 | n = el("mover", base, sup) |
| |
326 | case sub != nil && sup != nil: |
| |
327 | n = el("msubsup", base, sub, sup) |
| |
328 | case sub != nil: |
| |
329 | n = el("msub", base, sub) |
| |
330 | default: |
| |
331 | n = el("msup", base, sup) |
| |
332 | } |
| |
333 | n.fn = fn |
| |
334 | return n |
| |
335 | } |
| |
336 | |
| |
337 | // arg parses a command's argument: a braced group, or else one token. |
| |
338 | func (p *parser) arg() (*node, error) { |
| |
339 | t := p.peek() |
| |
340 | switch { |
| |
341 | case t.kind == eof: |
| |
342 | return nil, p.unexpected(t) |
| |
343 | case t.kind == char && strings.Contains("}&^_", t.val): |
| |
344 | return nil, p.unexpected(t) |
| |
345 | case t.kind == char && t.val >= "0" && t.val <= "9": |
| |
346 | p.next() |
| |
347 | return leaf("mn", p.styled(t.val)), nil |
| |
348 | } |
| |
349 | return p.atom() |
| |
350 | } |
| |
351 | |
| |
352 | func (p *parser) enter() error { |
| |
353 | p.depth++ |
| |
354 | if p.depth > MaxDepth { |
| |
355 | return ErrTooDeep |
| |
356 | } |
| |
357 | return nil |
| |
358 | } |
| |
359 | |
| |
360 | // atom parses one atom. A nil node with a nil error is a command that |
| |
361 | // renders nothing, such as \displaystyle. |
| |
362 | func (p *parser) atom() (*node, error) { |
| |
363 | if err := p.enter(); err != nil { |
| |
364 | return nil, err |
| |
365 | } |
| |
366 | defer func() { p.depth-- }() |
| |
367 | t := p.next() |
| |
368 | switch t.kind { |
| |
369 | case eof: |
| |
370 | return nil, p.unexpected(t) |
| |
371 | case char: |
| |
372 | return p.charAtom(t) |
| |
373 | } |
| |
374 | return p.command(t) |
| |
375 | } |
| |
376 | |
| |
377 | func (p *parser) charAtom(t token) (*node, error) { |
| |
378 | r, _ := utf8.DecodeRuneInString(t.val) |
| |
379 | switch { |
| |
380 | case t.val == "{": |
| |
381 | font := p.font |
| |
382 | kids, err := p.list(func(t token) bool { return isChar(t, "}") }) |
| |
383 | p.font = font |
| |
384 | if err != nil { |
| |
385 | return nil, err |
| |
386 | } |
| |
387 | if err := p.expect(char, "}"); err != nil { |
| |
388 | return nil, err |
| |
389 | } |
| |
390 | return row(kids), nil |
| |
391 | case r >= '0' && r <= '9': |
| |
392 | num := t.val |
| |
393 | for p.pos < len(p.src) { |
| |
394 | c := p.src[p.pos] |
| |
395 | if c >= '0' && c <= '9' || c == '.' && p.pos+1 < len(p.src) && p.src[p.pos+1] >= '0' && p.src[p.pos+1] <= '9' { |
| |
396 | num += string(c) |
| |
397 | p.pos++ |
| |
398 | continue |
| |
399 | } |
| |
400 | break |
| |
401 | } |
| |
402 | return leaf("mn", p.styled(num)), nil |
| |
403 | case unicode.IsLetter(r): |
| |
404 | n := leaf("mi", p.styled(t.val)) |
| |
405 | if p.font == "rm" { |
| |
406 | n.attr("mathvariant", "normal") |
| |
407 | } |
| |
408 | return n, nil |
| |
409 | } |
| |
410 | switch t.val { |
| |
411 | case "}", "&", "$", "#", "\\": |
| |
412 | return nil, p.unexpected(t) |
| |
413 | case "(", ")", "[", "]", "|", "/": |
| |
414 | return fixed(t.val), nil |
| |
415 | case "-": |
| |
416 | return leaf("mo", "−"), nil |
| |
417 | case "*": |
| |
418 | return leaf("mo", "∗"), nil |
| |
419 | case "'": |
| |
420 | return leaf("mo", "′"), nil |
| |
421 | case "~": |
| |
422 | return space("0.3333em"), nil |
| |
423 | } |
| |
424 | if r < 0x20 || r == utf8.RuneError { |
| |
425 | return nil, p.unexpected(t) |
| |
426 | } |
| |
427 | return leaf("mo", t.val), nil |
| |
428 | } |
| |
429 | |
| |
430 | func (p *parser) command(t token) (*node, error) { |
| |
431 | name := t.val |
| |
432 | if s, ok := symbols[name]; ok { |
| |
433 | n := leaf(s.tag, s.text) |
| |
434 | if s.upright { |
| |
435 | n.attr("mathvariant", "normal") |
| |
436 | } |
| |
437 | if s.fixed { |
| |
438 | n.attr("stretchy", "false") |
| |
439 | } |
| |
440 | n.limits = s.limits |
| |
441 | return n, nil |
| |
442 | } |
| |
443 | if w, ok := spaces[name]; ok { |
| |
444 | return space(w), nil |
| |
445 | } |
| |
446 | if f, ok := functions[name]; ok { |
| |
447 | n := leaf("mi", name) |
| |
448 | n.limits, n.fn = f, true |
| |
449 | return n, nil |
| |
450 | } |
| |
451 | if a, ok := accents[name]; ok { |
| |
452 | arg, err := p.arg() |
| |
453 | if err != nil { |
| |
454 | return nil, err |
| |
455 | } |
| |
456 | if a.under { |
| |
457 | return el("munder", arg, leaf("mo", a.mark)), nil |
| |
458 | } |
| |
459 | n := el("mover", arg, leaf("mo", a.mark)) |
| |
460 | if a.accent { |
| |
461 | n.attr("accent", "true") |
| |
462 | } |
| |
463 | return n, nil |
| |
464 | } |
| |
465 | if f, ok := fonts[name]; ok { |
| |
466 | font := p.font |
| |
467 | p.font = f |
| |
468 | arg, err := p.arg() |
| |
469 | p.font = font |
| |
470 | return arg, err |
| |
471 | } |
| |
472 | switch name { |
| |
473 | case "frac", "dfrac", "tfrac", "cfrac": |
| |
474 | num, err := p.arg() |
| |
475 | if err != nil { |
| |
476 | return nil, err |
| |
477 | } |
| |
478 | den, err := p.arg() |
| |
479 | if err != nil { |
| |
480 | return nil, err |
| |
481 | } |
| |
482 | return el("mfrac", num, den), nil |
| |
483 | case "binom", "dbinom", "tbinom": |
| |
484 | top, err := p.arg() |
| |
485 | if err != nil { |
| |
486 | return nil, err |
| |
487 | } |
| |
488 | bottom, err := p.arg() |
| |
489 | if err != nil { |
| |
490 | return nil, err |
| |
491 | } |
| |
492 | frac := el("mfrac", top, bottom).attr("linethickness", "0") |
| |
493 | return row([]*node{leaf("mo", "("), frac, leaf("mo", ")")}), nil |
| |
494 | case "sqrt": |
| |
495 | var index *node |
| |
496 | if isChar(p.peek(), "[") { |
| |
497 | p.next() |
| |
498 | kids, err := p.list(func(t token) bool { return isChar(t, "]") }) |
| |
499 | if err != nil { |
| |
500 | return nil, err |
| |
501 | } |
| |
502 | if err := p.expect(char, "]"); err != nil { |
| |
503 | return nil, err |
| |
504 | } |
| |
505 | index = row(kids) |
| |
506 | } |
| |
507 | arg, err := p.arg() |
| |
508 | if err != nil { |
| |
509 | return nil, err |
| |
510 | } |
| |
511 | if index != nil { |
| |
512 | return el("mroot", arg, index), nil |
| |
513 | } |
| |
514 | return el("msqrt", arg), nil |
| |
515 | case "left": |
| |
516 | open, err := p.delimiter() |
| |
517 | if err != nil { |
| |
518 | return nil, err |
| |
519 | } |
| |
520 | kids, err := p.list(func(t token) bool { return isCmd(t, "right") }) |
| |
521 | if err != nil { |
| |
522 | return nil, err |
| |
523 | } |
| |
524 | if err := p.expect(cmd, "right"); err != nil { |
| |
525 | return nil, err |
| |
526 | } |
| |
527 | cls, err := p.delimiter() |
| |
528 | if err != nil { |
| |
529 | return nil, err |
| |
530 | } |
| |
531 | var out []*node |
| |
532 | if open != "" { |
| |
533 | out = append(out, leaf("mo", open)) |
| |
534 | } |
| |
535 | out = append(out, kids...) |
| |
536 | if cls != "" { |
| |
537 | out = append(out, leaf("mo", cls)) |
| |
538 | } |
| |
539 | return row(out), nil |
| |
540 | case "middle": |
| |
541 | d, err := p.delimiter() |
| |
542 | if err != nil { |
| |
543 | return nil, err |
| |
544 | } |
| |
545 | return leaf("mo", d), nil |
| |
546 | case "text", "textrm", "textnormal", "mbox": |
| |
547 | s, err := p.rawGroup() |
| |
548 | if err != nil { |
| |
549 | return nil, err |
| |
550 | } |
| |
551 | return leaf("mtext", s), nil |
| |
552 | case "operatorname", "operatorname*": |
| |
553 | s, err := p.rawGroup() |
| |
554 | if err != nil { |
| |
555 | return nil, err |
| |
556 | } |
| |
557 | n := leaf("mi", s) |
| |
558 | if utf8.RuneCountInString(s) == 1 { |
| |
559 | n.attr("mathvariant", "normal") |
| |
560 | } |
| |
561 | n.limits, n.fn = name == "operatorname*", true |
| |
562 | return n, nil |
| |
563 | case "begin": |
| |
564 | return p.environment() |
| |
565 | case "displaystyle", "textstyle", "scriptstyle", "scriptscriptstyle", "limits", "nolimits": |
| |
566 | return nil, nil |
| |
567 | } |
| |
568 | return nil, p.unexpected(t) |
| |
569 | } |
| |
570 | |
| |
571 | // rawGroup reads a braced argument as text, for \text and \operatorname. |
| |
572 | // Nested braces must balance; \{, \}, \$, \%, \&, \#, \_ and \\ stand for |
| |
573 | // the character. |
| |
574 | func (p *parser) rawGroup() (string, error) { |
| |
575 | if err := p.expect(char, "{"); err != nil { |
| |
576 | return "", err |
| |
577 | } |
| |
578 | var b strings.Builder |
| |
579 | depth := 0 |
| |
580 | for p.pos < len(p.src) { |
| |
581 | c := p.src[p.pos] |
| |
582 | switch { |
| |
583 | case c == '\\' && p.pos+1 < len(p.src) && strings.IndexByte(`{}$%&#_\`, p.src[p.pos+1]) >= 0: |
| |
584 | b.WriteByte(p.src[p.pos+1]) |
| |
585 | p.pos += 2 |
| |
586 | continue |
| |
587 | case c == '{': |
| |
588 | depth++ |
| |
589 | case c == '}': |
| |
590 | if depth == 0 { |
| |
591 | p.pos++ |
| |
592 | return b.String(), nil |
| |
593 | } |
| |
594 | depth-- |
| |
595 | } |
| |
596 | b.WriteByte(c) |
| |
597 | p.pos++ |
| |
598 | } |
| |
599 | return "", errors.New("texmath: unterminated group") |
| |
600 | } |
| |
601 | |
| |
602 | // delimiter reads what follows \left, \middle or \right; "." is none. |
| |
603 | func (p *parser) delimiter() (string, error) { |
| |
604 | t := p.next() |
| |
605 | if t.kind == char { |
| |
606 | switch t.val { |
| |
607 | case ".": |
| |
608 | return "", nil |
| |
609 | case "<": |
| |
610 | return "⟨", nil |
| |
611 | case ">": |
| |
612 | return "⟩", nil |
| |
613 | case "(", ")", "[", "]", "|", "/": |
| |
614 | return t.val, nil |
| |
615 | } |
| |
616 | } |
| |
617 | if t.kind == cmd { |
| |
618 | if d, ok := delimiters[t.val]; ok { |
| |
619 | return d, nil |
| |
620 | } |
| |
621 | } |
| |
622 | return "", p.unexpected(t) |
| |
623 | } |
| |
624 | |
| |
625 | type env struct { |
| |
626 | open, close string |
| |
627 | table bool |
| |
628 | } |
| |
629 | |
| |
630 | var environments = map[string]env{ |
| |
631 | "matrix": {table: true}, |
| |
632 | "smallmatrix": {table: true}, |
| |
633 | "pmatrix": {"(", ")", true}, |
| |
634 | "bmatrix": {"[", "]", true}, |
| |
635 | "Bmatrix": {"{", "}", true}, |
| |
636 | "vmatrix": {"|", "|", true}, |
| |
637 | "Vmatrix": {"‖", "‖", true}, |
| |
638 | "cases": {"{", "", true}, |
| |
639 | "aligned": {table: true}, |
| |
640 | "align": {table: true}, |
| |
641 | "align*": {table: true}, |
| |
642 | "gathered": {table: true}, |
| |
643 | "gather": {table: true}, |
| |
644 | "gather*": {table: true}, |
| |
645 | "split": {table: true}, |
| |
646 | "equation": {}, |
| |
647 | "equation*": {}, |
| |
648 | "displaymath": {}, |
| |
649 | } |
| |
650 | |
| |
651 | func (p *parser) envName() (string, error) { |
| |
652 | if err := p.expect(char, "{"); err != nil { |
| |
653 | return "", err |
| |
654 | } |
| |
655 | end := strings.IndexByte(p.src[p.pos:], '}') |
| |
656 | if end < 0 { |
| |
657 | return "", errors.New("texmath: unterminated environment name") |
| |
658 | } |
| |
659 | name := p.src[p.pos : p.pos+end] |
| |
660 | p.pos += end + 1 |
| |
661 | return name, nil |
| |
662 | } |
| |
663 | |
| |
664 | func (p *parser) environment() (*node, error) { |
| |
665 | name, err := p.envName() |
| |
666 | if err != nil { |
| |
667 | return nil, err |
| |
668 | } |
| |
669 | e, ok := environments[name] |
| |
670 | if !ok { |
| |
671 | return nil, fmt.Errorf("texmath: unsupported environment %q", name) |
| |
672 | } |
| |
673 | var body *node |
| |
674 | if e.table { |
| |
675 | body, err = p.table() |
| |
676 | } else { |
| |
677 | var kids []*node |
| |
678 | kids, err = p.list(func(t token) bool { return isCmd(t, "end") }) |
| |
679 | body = row(kids) |
| |
680 | } |
| |
681 | if err != nil { |
| |
682 | return nil, err |
| |
683 | } |
| |
684 | if err := p.expect(cmd, "end"); err != nil { |
| |
685 | return nil, err |
| |
686 | } |
| |
687 | if end, err := p.envName(); err != nil { |
| |
688 | return nil, err |
| |
689 | } else if end != name { |
| |
690 | return nil, fmt.Errorf(`texmath: \begin{%s} ended by \end{%s}`, name, end) |
| |
691 | } |
| |
692 | if e.open == "" && e.close == "" { |
| |
693 | return body, nil |
| |
694 | } |
| |
695 | out := []*node{} |
| |
696 | if e.open != "" { |
| |
697 | out = append(out, leaf("mo", e.open)) |
| |
698 | } |
| |
699 | out = append(out, body) |
| |
700 | if e.close != "" { |
| |
701 | out = append(out, leaf("mo", e.close)) |
| |
702 | } |
| |
703 | return row(out), nil |
| |
704 | } |
| |
705 | |
| |
706 | // table parses rows separated by \\ and cells separated by &, up to \end. |
| |
707 | func (p *parser) table() (*node, error) { |
| |
708 | stop := func(t token) bool { return isChar(t, "&") || isCmd(t, "\\") || isCmd(t, "end") } |
| |
709 | tbl := el("mtable") |
| |
710 | cur := el("mtr") |
| |
711 | for { |
| |
712 | kids, err := p.list(stop) |
| |
713 | if err != nil { |
| |
714 | return nil, err |
| |
715 | } |
| |
716 | cur.kids = append(cur.kids, el("mtd", kids...)) |
| |
717 | t := p.peek() |
| |
718 | switch { |
| |
719 | case isChar(t, "&"): |
| |
720 | p.next() |
| |
721 | case isCmd(t, "\\"): |
| |
722 | p.next() |
| |
723 | tbl.kids = append(tbl.kids, cur) |
| |
724 | cur = el("mtr") |
| |
725 | case isCmd(t, "end"): |
| |
726 | // A trailing \\ leaves one empty cell; it is not a row. |
| |
727 | if len(cur.kids) > 1 || len(cur.kids[0].kids) > 0 { |
| |
728 | tbl.kids = append(tbl.kids, cur) |
| |
729 | } |
| |
730 | return tbl, nil |
| |
731 | default: |
| |
732 | return nil, p.unexpected(t) |
| |
733 | } |
| |
734 | } |
| |
735 | } |
| |
736 | |
| |
737 | // styled maps letters and digits into the current font's Mathematical |
| |
738 | // Alphanumeric Symbols, which is how MathML Core spells \mathbb and the rest: |
| |
739 | // mathvariant is honoured only as "normal". |
| |
740 | func (p *parser) styled(s string) string { |
| |
741 | if p.font == "" || p.font == "rm" { |
| |
742 | return s |
| |
743 | } |
| |
744 | var b strings.Builder |
| |
745 | for _, r := range s { |
| |
746 | b.WriteRune(alphanumeric(p.font, r)) |
| |
747 | } |
| |
748 | return b.String() |
| |
749 | } |