| @@ -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 | } |