Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 33 additions & 13 deletions math.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,17 +260,6 @@ func (e *Engine) renderMathResolvingMacros(r *texmath.Renderer, src string, disp
continue
}
next, ok := e.expandMacroInMathSource(src, name)
if ok {
// The body just spliced in is TeX, not maths: a class's \the@inst is
// \number\c@inst, an affiliation mark is an \@for loop over \expandafter
// and \edef. Those primitives are the ENGINE's to run, and the maths layer
// can only refuse them. Run the substituted material through the gullet —
// bounded to what was spliced, and unable to read past it since an isolated
// expansion stops at its own sentinel.
if flat := e.toksToString(e.expandList(tokenizeTeX(next))); flat != "" {
next = flat
}
}
if !ok {
// Colour commands (\color, \textcolor, …) are primitives go-tex/math
// cannot render. Strip them from the source — keeping any content
Expand Down Expand Up @@ -338,7 +327,7 @@ func (e *Engine) expandMacroInMathSource(src, name string) (string, bool) {
return "", false
}
if len(m.params) == 0 {
return replaceMathCS(src, name, e.toksToString(m.body)), true
return replaceMathCS(src, name, e.flattenMathBody(e.toksToString(m.body))), true
}
needle := "\\" + name + " "
n := len(m.params)
Expand All @@ -362,7 +351,7 @@ func (e *Engine) expandMacroInMathSource(src, name string) (string, bool) {
src = rest
continue
}
out.WriteString(e.substituteMathBody(m.body, args))
out.WriteString(e.flattenMathBody(e.substituteMathBody(m.body, args)))
src = rest[consumed:]
changed = true
}
Expand Down Expand Up @@ -971,6 +960,37 @@ func (e *Engine) substituteMathBody(body []tok, args []string) string {
return b.String()
}

// flattenMathBody runs a substituted macro body through the gullet, as \edef would.
// A class's body is TeX, not maths — \the@inst is \number\c@inst, an author's mark
// is an \@for loop over \expandafter and \edef — and the maths layer can only refuse
// those primitives, which costs the whole formula.
//
// Only the body just substituted goes through, never the surrounding source: the
// source holds commands the maths layer renders itself, and the engine's text-mode
// stand-ins for them would win. \cdot is \char183 here (latex.go) and \begin is
//
// \def\begin#1{\gotex@checkenv{#1}\csname #1\endcsname}
//
// so flattening the whole source turned every \cdot into \char and every
// \begin{bmatrix} into \bmatrix — 935 dropped formulas over 45 papers for the first,
// 676 over 42 for the second, and one paper typeset its matrices as text, 411251
// glyphs on seven pages.
//
// \begin and \end are still marked \noexpand inside the body itself, where the same
// reasoning applies to a macro that wraps an environment.
func (e *Engine) flattenMathBody(body string) string {
ts := tokenizeTeX(body)
for i, t := range ts {
if t.cs_ && (t.cs == "begin" || t.cs == "end") {
ts[i].noexp = true
}
}
if flat := e.toksToString(e.expandList(ts)); flat != "" {
return flat
}
return body
}

// unknownMathCommand extracts the command name X from a go-tex/math "unknown command
// \X" error, or "" when the message is a different failure. The name runs over letters
// and @ (a LaTeX-internal control word), matching a TeX control-word token.
Expand Down
33 changes: 33 additions & 0 deletions mathprotectbegin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) the go-tex/engine authors.
// SPDX-License-Identifier: BSD-3-Clause

package engine

import "testing"

// A substituted macro body is flattened through the gullet (#179) — but \begin and
// \end are macros here:
//
// \def\begin#1{\gotex@checkenv{#1}\csname #1\endcsname}
//
// so expanding them turns \begin{bmatrix} into \bmatrix, a control sequence the
// maths layer has never heard of, where the environment it wrote is one it renders.
// A paper's own \newcommand{\mymat}[2]{\begin{bmatrix}#1\\#2\end{bmatrix}} lost every
// formula that used it; over the corpus, bmatrix, pmatrix, cases, aligned, split and
// array accounted for 676 fresh drops in 42 papers.
func TestMathKeepsAnEnvironmentInsideAMacro(t *testing.T) {
for _, c := range []struct{ nom, def, math string }{
{"bmatrix", `\newcommand{\mymat}[2]{\begin{bmatrix}#1\\#2\end{bmatrix}}`, `A=\mymat{a}{b}`},
{"cases", `\newcommand{\mycase}[1]{\begin{cases}#1 & x>0\end{cases}}`, `f=\mycase{1}`},
{"array", `\newcommand{\myarr}[1]{\begin{array}{c}#1\end{array}}`, `M=\myarr{z}`},
} {
e, err := compile([]byte(`\documentclass{article}\usepackage{amsmath}`+c.def+
`\begin{document}$`+c.math+`$\end{document}`), Options{Lenient: true})
if err != nil {
t.Fatalf("%s: %v", c.nom, err)
}
if len(e.mathDropped) != 0 {
t.Errorf("%s: la couche maths a refusé la formule (%v)", c.nom, e.mathDropped)
}
}
}
Loading