diff --git a/internal/tex/matrix_command.go b/internal/tex/matrix_command.go index b96736b..c4ca5df 100644 --- a/internal/tex/matrix_command.go +++ b/internal/tex/matrix_command.go @@ -105,11 +105,13 @@ func (p *parser) matrixCommand(name string) ([]*mml.Node, error) { } spacing.apply(table, initial) table = finishArrayRules(table) + // ArrayItem.createMml uses ParseUtil.fenced, whose open/close properties + // classify the row as INNER and preserve spacing from its predecessor. if name == "pmatrix" { - return []*mml.Node{p.fenced("(", table, ")", true)}, nil + return []*mml.Node{p.leftRightFenced("(", table, ")", true)}, nil } if name == "cases" { - return []*mml.Node{p.fenced("{", table, "", true)}, nil + return []*mml.Node{p.leftRightFenced("{", table, "", true)}, nil } return []*mml.Node{table}, nil } diff --git a/matrix_fence_spacing_test.go b/matrix_fence_spacing_test.go new file mode 100644 index 0000000..9f7fbb2 --- /dev/null +++ b/matrix_fence_spacing_test.go @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +package mathjax_test + +import ( + "compress/gzip" + "encoding/json" + "os" + "strings" + "testing" + + mathjax "github.com/d2lang/mathjax-go" +) + +func TestLegacyMatrixFenceSpacingOriginalReferences(t *testing.T) { + file, err := os.Open("testdata/matrix_fence_spacing_mathjax_3_2_2.json.gz") + if err != nil { + t.Fatal(err) + } + defer file.Close() + reader, err := gzip.NewReader(file) + if err != nil { + t.Fatal(err) + } + defer reader.Close() + var fixture struct { + MathjaxGitCommit string + FreshOriginalPerCase bool + Counts struct { + Total, FullSVGAssertions, Valid, Diagnostics, QualifiedDiagnostics, OriginalRuntimeErrors int + } + Cases []struct { + Name, TeX, SVG string + Display, OriginalValid bool + } + QualifiedDiagnostics []struct { + Original struct{ SVG string } + } + OriginalRuntimeErrors []json.RawMessage + } + if err := json.NewDecoder(reader).Decode(&fixture); err != nil { + t.Fatal(err) + } + if fixture.MathjaxGitCommit != "ad8f5c21cb810236551da8c6512ba733e67357ee" || !fixture.FreshOriginalPerCase || + fixture.Counts.Total != 1236 || fixture.Counts.FullSVGAssertions != 1220 || fixture.Counts.Valid != 1156 || + fixture.Counts.Diagnostics != 64 || fixture.Counts.QualifiedDiagnostics != 16 || fixture.Counts.OriginalRuntimeErrors != 0 || + len(fixture.Cases) != 1220 || len(fixture.QualifiedDiagnostics) != 16 || len(fixture.OriginalRuntimeErrors) != 0 { + t.Fatal("unbound legacy Matrix fence references") + } + for _, c := range fixture.QualifiedDiagnostics { + if !strings.Contains(c.Original.SVG, `data-mml-node="merror"`) || !strings.Contains(c.Original.SVG, `data-mjx-error="Undefined control sequence \boldsymbol"`) { + t.Fatal("qualified original is not the retained unsupported-command diagnostic") + } + } + seen := map[string]bool{} + valid := 0 + for _, c := range fixture.Cases { + if c.Name == "" || c.SVG == "" || seen[c.Name] || c.OriginalValid == strings.Contains(c.SVG, `data-mml-node="merror"`) { + t.Fatal("invalid original reference", c.Name) + } + seen[c.Name] = true + if c.OriginalValid { + valid++ + } + t.Run(c.Name, func(t *testing.T) { + options := mathjax.DefaultOptions() + options.Display = c.Display + got, err := mathjax.RenderWithOptions(c.TeX, options) + if err != nil { + t.Fatal(err) + } + if got != c.SVG { + t.Fatalf("complete original SVG differs\ngot: %s\nwant: %s", got, c.SVG) + } + }) + } + if valid != fixture.Counts.Valid { + t.Fatalf("valid originals: got %d, want %d", valid, fixture.Counts.Valid) + } +} diff --git a/testdata/generate_matrix_fence_spacing.py b/testdata/generate_matrix_fence_spacing.py new file mode 100644 index 0000000..03a2b5a --- /dev/null +++ b/testdata/generate_matrix_fence_spacing.py @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: Apache-2.0 +# Regenerate only from D2's frozen original. Usage: +# python3 testdata/generate_matrix_fence_spacing.py PINNED_ASSETS --node NODE +import argparse,gzip,hashlib,json,pathlib,subprocess +HERE=pathlib.Path(__file__).resolve().parent +p=argparse.ArgumentParser() +p.add_argument('assets',type=pathlib.Path) +p.add_argument('--node',default='node') +a=p.parse_args() +hashes={'polyfills.js':'7fe1d048c78b0e09854c1259f7413868a51cc8f0c822489eb1ac36ae6c85ce01','mathjax.js':'cbbc1051a1f8abb1a181b6aa0fe927c020e3631ca630d19f52d9abb65b5ee869','setup.js':'a52cb0bbabfbd7796b9fedd793b9386c474e3123e788ee151cbe47ca1fa6e881'} +for name,want in hashes.items(): + if hashlib.sha256((a.assets/name).read_bytes()).hexdigest()!=want:raise ValueError('unverified '+name) +cases=[]; seen=set() +def add(group,name,tex): + for display in [False,True]: + key=(tex,display) + if key in seen:continue + seen.add(key);cases.append(dict(group=group,name=f'{group}-{name}-'+('display' if display else 'inline'),tex=tex,display=display)) +commands=['matrix','array','pmatrix','cases','eqalign','displaylines','eqalignno','leqalignno'] +def matrix(name,body=None): + if body is None:body=r'x&if x>0\cr 0&otherwise' if name=='cases' else r'a&b\cr c&d' + return '\\'+name+'{'+body+'}' +neighbors={'text':r'\text{Matrix: }','textplain':r'\text{label}','ord':'x','number':'2','bin':'+','rel':'=','op':r'\sum','function':r'\sin','open':'(','close':')','punct':',','inner':r'\mathinner{z}','forced-ord':r'\mathord{z}','forced-op':r'\mathop{z}','forced-bin':r'\mathbin{z}','forced-rel':r'\mathrel{z}','forced-open':r'\mathopen{(}','forced-close':r'\mathclose{)}','forced-punct':r'\mathpunct{,}','space':r'\quad','fraction':r'\frac{p}{q}'} +for name in commands: + m=matrix(name) + add('bare',name,m) + for label,n in neighbors.items(): + add('preceding',name+'-'+label,n+m) + add('following',name+'-'+label,m+n) + for label,left,right in [('text',r'\text{Matrix: }',r'\text{ done}'),('ord','x','y'),('bin','x+','+y'),('rel','x=','=y'),('op',r'\sum',r'\sum'),('function',r'\sin',r'\cos'),('inner',r'\mathinner{z}',r'\mathinner{w}')]:add('surrounding',name+'-'+label,left+m+right) + for label,tex in [('mathsf',r'\mathsf{x'+m+'y}'),('mathbf',r'\mathbf{x'+m+'y}'),('mathit',r'\mathit{x'+m+'y}'),('boldsymbol',r'\boldsymbol{x'+m+'y}'),('sup',r'X^{x'+m+'y}'),('sub',r'X_{x'+m+'y}'),('denominator',r'\frac{z}{x'+m+'y}'),('root',r'\sqrt{x'+m+'y}'),('postscript',r'x'+m+r'_i^jy'),('braced',r'x{'+m+'}y'),('scriptstyle',r'\scriptstyle x'+m+'y'),('textstyle',r'\textstyle x'+m+'y')]:add('scope',name+'-'+label,tex) + for label,body in [('single','a'),('empty',''),('fraction',r'\frac{a}{b}&\sqrt{c}\cr g&h'),('font',r'\mathbf{a}&\mathsf{b}\cr c&d')]: + if name=='cases' and label in ['fraction','font']:continue + add('body',name+'-'+label,r'x'+matrix(name,body)+'y') +for env in ['matrix','pmatrix','bmatrix','Bmatrix','vmatrix','Vmatrix','cases','smallmatrix','array','aligned']: + m='\\begin{'+env+'}'+('{cc}' if env=='array' else '')+r'a&b\\c&d'+'\\end{'+env+'}' + for label,tex in [('bare',m),('text',r'\text{Matrix: }'+m),('surrounding','x'+m+'y'),('operator',r'\sin'+m+r'\cos'),('font',r'\mathbf{x'+m+'y}'),('script',r'X^{x'+m+'y}')]:add('environment',env+'-'+label,tex) +for name in commands: + for label,tex in [('missing','\\'+name),('close','\\'+name+'{a'),('unbraced-script','X^\\'+name+'{a}'),('extra-close','\\'+name+'{a}}')]:add('diagnostic',name+'-'+label,tex) + +records=[];qualified=[];runtime=[] +for start in range(0,len(cases),24): + batch=cases[start:start+24] + inputs=''.join(json.dumps({'tex':c['tex'],'options':{'Display':c['display'],'Em':16,'Ex':8}})+'\n' for c in batch) + run=subprocess.run([a.node,'--jitless',str(HERE/'differential/oracle.mjs'),'--asset-dir',str(a.assets)],input=inputs,text=True,capture_output=True,check=True) + outputs=[json.loads(line) for line in run.stdout.rstrip('\n').split('\n')] + if len(outputs)!=len(batch):raise ValueError('incomplete original output') + for c,original in zip(batch,outputs): + if original.get('error'):runtime.append({**c,'original':original});continue + svg=original['svg'];valid='data-mml-node="merror"' not in svg + if r'data-mjx-error="Undefined control sequence \boldsymbol"' in svg: + if valid:raise ValueError('qualified diagnostic is valid') + qualified.append({**c,'original':original,'qualification':'Frozen D2 original does not register \\boldsymbol, while Go accepts it; excluded from exact SVG assertions and parity credit.'}) + else:records.append({**c,'originalValid':valid,'svg':svg}) + print('fresh original',min(start+24,len(cases)),'/',len(cases),flush=True) +fixture={'mathjaxGitCommit':'ad8f5c21cb810236551da8c6512ba733e67357ee','oracleAssetsSHA256':hashes,'freshOriginalPerCase':True,'counts':{'total':len(cases),'fullSVGAssertions':len(records),'valid':sum(c['originalValid'] for c in records),'diagnostics':sum(not c['originalValid'] for c in records),'qualifiedDiagnostics':len(qualified),'originalRuntimeErrors':len(runtime)},'cases':records,'qualifiedDiagnostics':qualified,'originalRuntimeErrors':runtime} +(HERE/'matrix_fence_spacing_mathjax_3_2_2.json.gz').write_bytes(gzip.compress((json.dumps(fixture,ensure_ascii=False,indent=2)+'\n').encode(),mtime=0)) diff --git a/testdata/matrix_fence_spacing_README.md b/testdata/matrix_fence_spacing_README.md new file mode 100644 index 0000000..6b278d9 --- /dev/null +++ b/testdata/matrix_fence_spacing_README.md @@ -0,0 +1,17 @@ +# Legacy Matrix fence spacing + +MathJax 3.2.2's `Base.Matrix` stores the `pmatrix` and `cases` fence pair on its ArrayItem. `ArrayItem.createMml` calls `ParseUtil.fenced`, which gives the enclosing mrow `open`, `close`, and `texClass: INNER` properties. Those properties preserve thin TeX spacing from preceding text, ordinary atoms, and operators. The Go legacy commands used a plain delimiter row and lost that spacing; the existing `leftRightFenced` constructor restores the original row contract. Matrix environments already use that contract. + +`matrix_fence_spacing_mathjax_3_2_2.json.gz` preserves 1,236 fresh original requests: 1,156 valid SVGs, 64 exact original diagnostics, and 16 separately retained original-unsupported `boldsymbol` diagnostics. The last group receives no exact-SVG assertion or parity credit because Go accepts that extra command. No original runtime failures occurred. All 1,220 strict SVG assertions match the fix; against canonical baseline `fc40841854f0b3cdad53ee346a71646c478b7ba8`, 98 valid outputs are fixed and 1,122 assertions remain unchanged. There are no SVG regressions. + +The corpus covers all eight legacy Matrix commands (`matrix`, `array`, `pmatrix`, `cases`, `eqalign`, `displaylines`, `eqalignno`, `leqalignno`), preceding and following atom classes, surrounding text and operators, font and script scopes, nested fractions and roots, matrix cell bodies, ten matrix/array environment controls, and original diagnostic boundaries. Inline and display mode are tested for every input. `matrix_fence_spacing_observations.json` independently records the frozen active registrations, original methods, and original fenced-row properties after rendering. + +Regenerate only with the hash-verified, unmodified D2 MathJax 3.2.2 assets; each request gets a fresh runtime: + +```sh +python3 testdata/generate_matrix_fence_spacing.py /path/to/pinned/assets --node /path/to/node +node --jitless testdata/observe_matrix_fence_spacing.cjs /path/to/pinned/assets +go test ./... -run TestLegacyMatrixFenceSpacingOriginalReferences -count=1 +``` + +The finite corpus establishes the measured fence spacing contract; it does not establish universal MathJax parity. diff --git a/testdata/matrix_fence_spacing_mathjax_3_2_2.json.gz b/testdata/matrix_fence_spacing_mathjax_3_2_2.json.gz new file mode 100644 index 0000000..a95cfdb Binary files /dev/null and b/testdata/matrix_fence_spacing_mathjax_3_2_2.json.gz differ diff --git a/testdata/matrix_fence_spacing_observations.json b/testdata/matrix_fence_spacing_observations.json new file mode 100644 index 0000000..2ed67f3 --- /dev/null +++ b/testdata/matrix_fence_spacing_observations.json @@ -0,0 +1,199 @@ +{ + "mathjaxGitCommit": "ad8f5c21cb810236551da8c6512ba733e67357ee", + "freshRuntimePerObservation": true, + "hashes": { + "polyfills.js": "7fe1d048c78b0e09854c1259f7413868a51cc8f0c822489eb1ac36ae6c85ce01", + "mathjax.js": "cbbc1051a1f8abb1a181b6aa0fe927c020e3631ca630d19f52d9abb65b5ee869", + "setup.js": "a52cb0bbabfbd7796b9fedd793b9386c474e3123e788ee151cbe47ca1fa6e881" + }, + "unsupportedOriginalCommand": { + "name": "boldsymbol", + "registered": false + }, + "source": { + "Matrix": "function(t,e,Q,r,T,n,o,i,a,s){var l=t.GetNext();if(\"\"===l)throw new c.default(\"MissingArgFor\",\"Missing argument for %1\",t.currentCS);\"{\"===l?t.i++:(t.string=l+\"}\"+t.string.slice(t.i+1),t.i=0);var u=t.itemFactory.create(\"array\").setProperty(\"requireClose\",!0);u.arraydef={rowspacing:o||\"4pt\",columnspacing:n||\"1em\"},a&&u.setProperty(\"isCases\",!0),s&&(u.setProperty(\"isNumbered\",!0),u.arraydef.side=s),(Q||r)&&(u.setProperty(\"open\",Q),u.setProperty(\"close\",r)),\"D\"===i&&(u.arraydef.displaystyle=!0),null!=T&&(u.arraydef.columnalign=T),t.Push(u)}", + "fenced": "function(t,e,Q,r,T,n){void 0===T&&(T=\"\"),void 0===n&&(n=\"\");var o,l=t.nodeFactory,c=l.create(\"node\",\"mrow\",[],{open:e,close:r,texClass:i.TEXCLASS.INNER});if(T)o=new s.default(\"\\\\\"+T+\"l\"+e,t.parser.stack.env,t).mml();else{var u=l.create(\"text\",e);o=l.create(\"node\",\"mo\",[],{fence:!0,stretchy:!0,symmetric:!0,texClass:i.TEXCLASS.OPEN},u)}if(a.default.appendChildren(c,[o,Q]),T)o=new s.default(\"\\\\\"+T+\"r\"+r,t.parser.stack.env,t).mml();else{var p=l.create(\"text\",r);o=l.create(\"node\",\"mo\",[],{fence:!0,stretchy:!0,symmetric:!0,texClass:i.TEXCLASS.CLOSE},p)}return n&&o.attributes.set(\"mathcolor\",n),a.default.appendChildren(c,[o]),c}", + "ArrayCreateMml": "function(){var t=this.arraydef.scriptlevel;delete this.arraydef.scriptlevel;var e=this.create(\"node\",\"mtable\",this.table,this.arraydef);return t&&e.setProperty(\"scriptlevel\",t),4===this.frame.length?p.default.setAttribute(e,\"frame\",this.dashed?\"dashed\":\"solid\"):this.frame.length&&(this.arraydef.rowlines&&(this.arraydef.rowlines=this.arraydef.rowlines.replace(/none( none)+$/,\"none\")),p.default.setAttribute(e,\"frame\",\"\"),e=this.create(\"node\",\"menclose\",[e],{notation:this.frame.join(\" \")}),\"none\"===(this.arraydef.columnlines||\"none\")&&\"none\"===(this.arraydef.rowlines||\"none\")||p.default.setAttribute(e,\"data-padding\",0)),(this.getProperty(\"open\")||this.getProperty(\"close\"))&&(e=u.default.fenced(this.factory.configuration,this.getProperty(\"open\"),e,this.getProperty(\"close\"))),e}" + }, + "commands": { + "matrix": { + "args": [], + "BaseMatrix": true + }, + "array": { + "args": [], + "BaseMatrix": true + }, + "pmatrix": { + "args": [ + "(", + ")" + ], + "BaseMatrix": true + }, + "cases": { + "args": [ + "{", + "", + "left left", + null, + ".1em", + null, + true + ], + "BaseMatrix": true + }, + "eqalign": { + "args": [ + null, + null, + "right left", + "0.278em", + ".5em", + "D" + ], + "BaseMatrix": true + }, + "displaylines": { + "args": [ + null, + null, + "center", + null, + ".5em", + "D" + ], + "BaseMatrix": true + }, + "eqalignno": { + "args": [ + null, + null, + "right left", + "0.278em", + ".5em", + "D", + null, + "right" + ], + "BaseMatrix": true + }, + "leqalignno": { + "args": [ + null, + null, + "right left", + "0.278em", + ".5em", + "D", + null, + "left" + ], + "BaseMatrix": true + } + }, + "observations": [ + { + "name": "pmatrix", + "tex": "\\text{Matrix: }\\pmatrix{a&b\\cr c&d}", + "svg": "", + "rows": [ + { + "kind": "mrow", + "properties": { + "open": "(", + "close": ")", + "texClass": 7 + }, + "texClass": 7, + "prevClass": 0, + "children": [ + { + "kind": "mo", + "texClass": 4, + "prevClass": -1 + }, + { + "kind": "mtable", + "texClass": 0, + "prevClass": 4 + }, + { + "kind": "mo", + "texClass": 5, + "prevClass": 0 + } + ] + } + ] + }, + { + "name": "cases", + "tex": "x\\cases{x&if x>0\\cr 0&otherwise}y", + "svg": "", + "rows": [ + { + "kind": "mrow", + "properties": { + "open": "{", + "close": "", + "texClass": 7 + }, + "texClass": 7, + "prevClass": 0, + "children": [ + { + "kind": "mo", + "texClass": 4, + "prevClass": -1 + }, + { + "kind": "mtable", + "texClass": 0, + "prevClass": 4 + }, + { + "kind": "mo", + "texClass": 5, + "prevClass": 0 + } + ] + } + ] + }, + { + "name": "environment", + "tex": "\\text{Matrix: }\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}", + "svg": "", + "rows": [ + { + "kind": "mrow", + "properties": { + "open": "(", + "close": ")", + "texClass": 7 + }, + "texClass": 7, + "prevClass": 0, + "children": [ + { + "kind": "mo", + "texClass": 4, + "prevClass": -1 + }, + { + "kind": "mtable", + "texClass": 0, + "prevClass": 4 + }, + { + "kind": "mo", + "texClass": 5, + "prevClass": 0 + } + ] + } + ] + } + ] +} diff --git a/testdata/observe_matrix_fence_spacing.cjs b/testdata/observe_matrix_fence_spacing.cjs new file mode 100644 index 0000000..55394ab --- /dev/null +++ b/testdata/observe_matrix_fence_spacing.cjs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +const fs=require('node:fs'),vm=require('node:vm'),crypto=require('node:crypto'); +const path=require('node:path'); +const assets=process.argv[2];if(!assets)throw Error('usage: node --jitless testdata/observe_matrix_fence_spacing.cjs PINNED_ASSETS'); +const hashes={'polyfills.js':'7fe1d048c78b0e09854c1259f7413868a51cc8f0c822489eb1ac36ae6c85ce01','mathjax.js':'cbbc1051a1f8abb1a181b6aa0fe927c020e3631ca630d19f52d9abb65b5ee869','setup.js':'a52cb0bbabfbd7796b9fedd793b9386c474e3123e788ee151cbe47ca1fa6e881'}; +const scripts=Object.entries(hashes).map(([name,hash])=>{const b=fs.readFileSync(assets+'/'+name);if(crypto.createHash('sha256').update(b).digest('hex')!==hash)throw Error('hash');return new vm.Script(b.toString(),{filename:name});}); +function fresh(){const c=vm.createContext({console});c.globalThis=c;for(const s of scripts)s.runInContext(c);return c;} +const c=fresh(),tex=c.MathJax._.input.tex,pu=tex.ParseUtil.default||tex.ParseUtil,items=tex.base.BaseItems; + +const info={mathjaxGitCommit:'ad8f5c21cb810236551da8c6512ba733e67357ee',freshRuntimePerObservation:true,hashes,unsupportedOriginalCommand:{name:'boldsymbol',registered:!!c.html.inputJax[0].parseOptions.handlers.get('macro').lookup('boldsymbol')},source:{Matrix:tex.base.BaseMethods.default.Matrix.toString(),fenced:pu.fenced.toString(),ArrayCreateMml:items.ArrayItem.prototype.createMml.toString()},commands:{},observations:[]}; +for(const name of ['matrix','array','pmatrix','cases','eqalign','displaylines','eqalignno','leqalignno']){const m=c.html.inputJax[0].parseOptions.handlers.get('macro').lookup(name);info.commands[name]={args:m.args,BaseMatrix:m.func===tex.base.BaseMethods.default.Matrix};} +for(const [name,t]of Object.entries({pmatrix:String.raw`\text{Matrix: }\pmatrix{a&b\cr c&d}`,cases:String.raw`x\cases{x&if x>0\cr 0&otherwise}y`,environment:String.raw`\text{Matrix: }\begin{pmatrix}a&b\\c&d\end{pmatrix}`})){ + const r=fresh(),p=r.MathJax._.input.tex.ParseUtil.default||r.MathJax._.input.tex.ParseUtil;const old=p.fenced,rows=[]; + p.fenced=function(...args){const n=old.apply(this,args);rows.push(n);return n;}; + const output=r.html.convert(t,{em:16,ex:8,display:true}); + info.observations.push({name,tex:t,svg:r.adaptor.innerHTML(output),rows:rows.map(n=>({kind:n.kind,properties:n.getAllProperties(),texClass:n.texClass,prevClass:n.prevClass,children:n.childNodes.map(ch=>({kind:ch.kind,texClass:ch.texClass,prevClass:ch.prevClass}))}))}); +} +fs.writeFileSync(path.join(__dirname,'matrix_fence_spacing_observations.json'),JSON.stringify(info,null,2)+'\n'); diff --git a/testdata/parity/legacy-matrix-spacing/README.md b/testdata/parity/legacy-matrix-spacing/README.md new file mode 100644 index 0000000..db095a2 --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/README.md @@ -0,0 +1,16 @@ +# Restore legacy matrix fence spacing + +The legacy `\pmatrix` command omitted the original thin space between the preceding `x` and its opening parenthesis. This D2 witness uses `\Huge x\pmatrix{1}y` with its caption outside the formula. The companion [cases witness](cases/README.md) demonstrates the other affected command. + +`comparison.png` shows three complete, untouched D2 SVGs at one shared scale. The fixed and independently rendered original SVGs and PNGs are byte-identical; the before output visibly lacks the spacing. Both comparisons were viewed and checked for clipping. See `manifest.json` for all source revisions and artifact hashes. The before binary was built from local preview `883f28d1180b49d1d1f9f7421eedc30f181d887d`; its entire tracked tree exactly matches canonical baseline `fc40841854f0b3cdad53ee346a71646c478b7ba8` (tree `090f9f0c9c35c7bdeeaec697177edbfb8107918b`). + +The fresh original corpus has 1,156 valid SVGs: 98 fixes and 1,058 unchanged controls. Another 64 original diagnostic SVGs are unchanged exact controls, for 1,220 strict full-SVG assertions. Sixteen original-unsupported `boldsymbol` diagnostics are preserved separately and receive no parity credit. There were zero original runtime failures and zero SVG regressions. The corpus covers all eight legacy Matrix commands, neighboring text and atom classes, inline/display mode, fonts and scripts, and ten environment controls. + +To regenerate, use D2 at the recorded renderer revision with an external modfile replacing `github.com/d2lang/mathjax-go` with the recorded before or fixed checkout, then render the exact source: + +```sh +go build -modfile /absolute/path/to/replaced-d2.mod -o /tmp/d2-parity . +/tmp/d2-parity --pad 24 --scale 1 --omit-version witness.d2 output.svg +``` + +For the original, build D2 with `testdata/parity/accent-italic-correction/oracle-adapter` as the replacement, then set `MATHJAX_GO_NODE`, `MATHJAX_GO_ORACLE_SCRIPT`, and `MATHJAX_GO_ORACLE_DIR` to Node, `testdata/differential/oracle.mjs`, and the pinned assets. These finite checks do not establish universal MathJax parity. diff --git a/testdata/parity/legacy-matrix-spacing/after.png b/testdata/parity/legacy-matrix-spacing/after.png new file mode 100644 index 0000000..6cc4cc6 Binary files /dev/null and b/testdata/parity/legacy-matrix-spacing/after.png differ diff --git a/testdata/parity/legacy-matrix-spacing/after.svg b/testdata/parity/legacy-matrix-spacing/after.svg new file mode 100644 index 0000000..3555615 --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/after.svg @@ -0,0 +1,27 @@ +Legacy pmatrix: spacing after x + + + diff --git a/testdata/parity/legacy-matrix-spacing/before.png b/testdata/parity/legacy-matrix-spacing/before.png new file mode 100644 index 0000000..b1f7195 Binary files /dev/null and b/testdata/parity/legacy-matrix-spacing/before.png differ diff --git a/testdata/parity/legacy-matrix-spacing/before.svg b/testdata/parity/legacy-matrix-spacing/before.svg new file mode 100644 index 0000000..21af97d --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/before.svg @@ -0,0 +1,27 @@ +Legacy pmatrix: spacing after x + + + diff --git a/testdata/parity/legacy-matrix-spacing/cases/README.md b/testdata/parity/legacy-matrix-spacing/cases/README.md new file mode 100644 index 0000000..d40e9d7 --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/cases/README.md @@ -0,0 +1,16 @@ +# Restore legacy matrix fence spacing + +The legacy `\cases` command loses the same original thin space before its brace. This secondary D2 witness uses `\Huge x\cases{1}y` with its caption outside the formula. + +`comparison.png` shows three complete, untouched D2 SVGs at one shared scale. The fixed and independently rendered original SVGs and PNGs are byte-identical; the before output visibly lacks the spacing. Both comparisons were viewed and checked for clipping. See `manifest.json` for all source revisions and artifact hashes. The before binary was built from local preview `883f28d1180b49d1d1f9f7421eedc30f181d887d`; its entire tracked tree exactly matches canonical baseline `fc40841854f0b3cdad53ee346a71646c478b7ba8` (tree `090f9f0c9c35c7bdeeaec697177edbfb8107918b`). + +The fresh original corpus has 1,156 valid SVGs: 98 fixes and 1,058 unchanged controls. Another 64 original diagnostic SVGs are unchanged exact controls, for 1,220 strict full-SVG assertions. Sixteen original-unsupported `boldsymbol` diagnostics are preserved separately and receive no parity credit. There were zero original runtime failures and zero SVG regressions. The corpus covers all eight legacy Matrix commands, neighboring text and atom classes, inline/display mode, fonts and scripts, and ten environment controls. + +To regenerate, use D2 at the recorded renderer revision with an external modfile replacing `github.com/d2lang/mathjax-go` with the recorded before or fixed checkout, then render the exact source: + +```sh +go build -modfile /absolute/path/to/replaced-d2.mod -o /tmp/d2-parity . +/tmp/d2-parity --pad 24 --scale 1 --omit-version witness.d2 output.svg +``` + +For the original, build D2 with `testdata/parity/accent-italic-correction/oracle-adapter` as the replacement, then set `MATHJAX_GO_NODE`, `MATHJAX_GO_ORACLE_SCRIPT`, and `MATHJAX_GO_ORACLE_DIR` to Node, `testdata/differential/oracle.mjs`, and the pinned assets. These finite checks do not establish universal MathJax parity. diff --git a/testdata/parity/legacy-matrix-spacing/cases/after.png b/testdata/parity/legacy-matrix-spacing/cases/after.png new file mode 100644 index 0000000..e2d9216 Binary files /dev/null and b/testdata/parity/legacy-matrix-spacing/cases/after.png differ diff --git a/testdata/parity/legacy-matrix-spacing/cases/after.svg b/testdata/parity/legacy-matrix-spacing/cases/after.svg new file mode 100644 index 0000000..b4191bd --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/cases/after.svg @@ -0,0 +1,27 @@ +Legacy cases: spacing after x + + + diff --git a/testdata/parity/legacy-matrix-spacing/cases/before.png b/testdata/parity/legacy-matrix-spacing/cases/before.png new file mode 100644 index 0000000..1c4d920 Binary files /dev/null and b/testdata/parity/legacy-matrix-spacing/cases/before.png differ diff --git a/testdata/parity/legacy-matrix-spacing/cases/before.svg b/testdata/parity/legacy-matrix-spacing/cases/before.svg new file mode 100644 index 0000000..7995658 --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/cases/before.svg @@ -0,0 +1,27 @@ +Legacy cases: spacing after x + + + diff --git a/testdata/parity/legacy-matrix-spacing/cases/comparison.png b/testdata/parity/legacy-matrix-spacing/cases/comparison.png new file mode 100644 index 0000000..890a054 Binary files /dev/null and b/testdata/parity/legacy-matrix-spacing/cases/comparison.png differ diff --git a/testdata/parity/legacy-matrix-spacing/cases/manifest.json b/testdata/parity/legacy-matrix-spacing/cases/manifest.json new file mode 100644 index 0000000..e678bb0 --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/cases/manifest.json @@ -0,0 +1,45 @@ +{ + "d2Commit": "01bc7ecdbdd04c13d6fe5df1967d2d9aa14ae579", + "beforeMathjaxCommit": "883f28d1180b49d1d1f9f7421eedc30f181d887d", + "beforeEquivalentCanonicalMathjaxCommit": "fc40841854f0b3cdad53ee346a71646c478b7ba8", + "beforeCanonicalTrackedTree": "090f9f0c9c35c7bdeeaec697177edbfb8107918b", + "fixedSourceCommit": "743a583dbe68f5257c2c476e5989bf874ac5b9db", + "original": "D2 frozen MathJax 3.2.2; unmodified hash-verified assets; fresh runtime per distinct formula", + "afterOriginalSVGByteExact": true, + "afterOriginalPNGByteExact": true, + "sharedScale": true, + "comparisonScale": 1.7708333333333333, + "allImagesFit": true, + "visuallyInspected": true, + "corpus": { + "originalRequests": 1236, + "validSVGs": 1156, + "validFixes": 98, + "validUnchangedControls": 1058, + "strictDiagnosticControls": 64, + "qualifiedOriginalUnsupportedBoldsymbolDiagnostics": 16, + "originalRuntimeFailures": 0, + "strictSVGAssertions": 1220, + "SVGRegressions": 0 + }, + "d2Binaries": { + "before": "e6a169296f3dd721c3826e302354d4df3500b9e1002bcf4dde1e6615f87cbe8f", + "fixed": "a1dc813f648b72fbb1ed84b42c408ed3a5dd5aa43e6eb19adf5c972bcc22eac9", + "original": "98f8224c202addd0de5b82344fb2ef00da06a001eac5cdd8dff49e3cb410c194" + }, + "files": { + "after.png": "16c4dde64991d7f93987ab39b40763287b045b53a5a815bc114f8eb798c27675", + "after.svg": "f2b6da75f3501a52ece4f3dd01376d672d8153ebafcf1b16c9e3283fdbc761c7", + "before.png": "8393459c685c820f41f9bd7ae5be5f9e3b07e37bfa3ac950e6beb402800b4a96", + "before.svg": "4414caf690d25e5ea5508d641fa7783ad8da10e2237c0551e8046f2b6e6e975a", + "comparison.png": "a3be6f0bf895ab03858378ed57449d25209deb1323f4eca8f39c214c5ccd2925", + "original.png": "16c4dde64991d7f93987ab39b40763287b045b53a5a815bc114f8eb798c27675", + "original.svg": "f2b6da75f3501a52ece4f3dd01376d672d8153ebafcf1b16c9e3283fdbc761c7", + "witness.d2": "e83e1d8278df87e156ef75a65ec0739a731907b10f8dac1749780a65feef3709" + }, + "oracleAssets": { + "mathjax.js": "cbbc1051a1f8abb1a181b6aa0fe927c020e3631ca630d19f52d9abb65b5ee869", + "polyfills.js": "7fe1d048c78b0e09854c1259f7413868a51cc8f0c822489eb1ac36ae6c85ce01", + "setup.js": "a52cb0bbabfbd7796b9fedd793b9386c474e3123e788ee151cbe47ca1fa6e881" + } +} diff --git a/testdata/parity/legacy-matrix-spacing/cases/original.png b/testdata/parity/legacy-matrix-spacing/cases/original.png new file mode 100644 index 0000000..e2d9216 Binary files /dev/null and b/testdata/parity/legacy-matrix-spacing/cases/original.png differ diff --git a/testdata/parity/legacy-matrix-spacing/cases/original.svg b/testdata/parity/legacy-matrix-spacing/cases/original.svg new file mode 100644 index 0000000..b4191bd --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/cases/original.svg @@ -0,0 +1,27 @@ +Legacy cases: spacing after x + + + diff --git a/testdata/parity/legacy-matrix-spacing/cases/witness.d2 b/testdata/parity/legacy-matrix-spacing/cases/witness.d2 new file mode 100644 index 0000000..be6be5c --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/cases/witness.d2 @@ -0,0 +1,8 @@ +grid-columns: 1 +caption: { + shape: text + label: "Legacy cases: spacing after x" +} +cases: |latex +\Huge x\cases{1}y +| diff --git a/testdata/parity/legacy-matrix-spacing/comparison.png b/testdata/parity/legacy-matrix-spacing/comparison.png new file mode 100644 index 0000000..b93693d Binary files /dev/null and b/testdata/parity/legacy-matrix-spacing/comparison.png differ diff --git a/testdata/parity/legacy-matrix-spacing/manifest.json b/testdata/parity/legacy-matrix-spacing/manifest.json new file mode 100644 index 0000000..925da4e --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/manifest.json @@ -0,0 +1,45 @@ +{ + "d2Commit": "01bc7ecdbdd04c13d6fe5df1967d2d9aa14ae579", + "beforeMathjaxCommit": "883f28d1180b49d1d1f9f7421eedc30f181d887d", + "beforeEquivalentCanonicalMathjaxCommit": "fc40841854f0b3cdad53ee346a71646c478b7ba8", + "beforeCanonicalTrackedTree": "090f9f0c9c35c7bdeeaec697177edbfb8107918b", + "fixedSourceCommit": "743a583dbe68f5257c2c476e5989bf874ac5b9db", + "original": "D2 frozen MathJax 3.2.2; unmodified hash-verified assets; fresh runtime per distinct formula", + "afterOriginalSVGByteExact": true, + "afterOriginalPNGByteExact": true, + "sharedScale": true, + "comparisonScale": 1.6666666666666667, + "allImagesFit": true, + "visuallyInspected": true, + "corpus": { + "originalRequests": 1236, + "validSVGs": 1156, + "validFixes": 98, + "validUnchangedControls": 1058, + "strictDiagnosticControls": 64, + "qualifiedOriginalUnsupportedBoldsymbolDiagnostics": 16, + "originalRuntimeFailures": 0, + "strictSVGAssertions": 1220, + "SVGRegressions": 0 + }, + "d2Binaries": { + "before": "e6a169296f3dd721c3826e302354d4df3500b9e1002bcf4dde1e6615f87cbe8f", + "fixed": "a1dc813f648b72fbb1ed84b42c408ed3a5dd5aa43e6eb19adf5c972bcc22eac9", + "original": "98f8224c202addd0de5b82344fb2ef00da06a001eac5cdd8dff49e3cb410c194" + }, + "files": { + "after.png": "0b38d44be540882f0a30332fdc083e276ff41f8a787571594ef8ff884ebf71ef", + "after.svg": "fb83b75ea136e49e8ae05ed82b006d3dd58af2405d7a62cd7238d0c0ef7f3b92", + "before.png": "7a36d1436819555461a1aa506965f57d4007527700476426a38f9f5db32fa3b9", + "before.svg": "17bb20f6762cdf1deec1e1ae3838b05ded50cd7d44327ee866bd3a4f242a4e79", + "comparison.png": "6c6e9173449fd150bf64d1e508af65d29269fb23384b1b8f50e2ece6b913cefb", + "original.png": "0b38d44be540882f0a30332fdc083e276ff41f8a787571594ef8ff884ebf71ef", + "original.svg": "fb83b75ea136e49e8ae05ed82b006d3dd58af2405d7a62cd7238d0c0ef7f3b92", + "witness.d2": "8f2f43b667a0f3eb48f696463f15261298ee0a8e3a221f289659b02cd865ad31" + }, + "oracleAssets": { + "mathjax.js": "cbbc1051a1f8abb1a181b6aa0fe927c020e3631ca630d19f52d9abb65b5ee869", + "polyfills.js": "7fe1d048c78b0e09854c1259f7413868a51cc8f0c822489eb1ac36ae6c85ce01", + "setup.js": "a52cb0bbabfbd7796b9fedd793b9386c474e3123e788ee151cbe47ca1fa6e881" + } +} diff --git a/testdata/parity/legacy-matrix-spacing/original.png b/testdata/parity/legacy-matrix-spacing/original.png new file mode 100644 index 0000000..6cc4cc6 Binary files /dev/null and b/testdata/parity/legacy-matrix-spacing/original.png differ diff --git a/testdata/parity/legacy-matrix-spacing/original.svg b/testdata/parity/legacy-matrix-spacing/original.svg new file mode 100644 index 0000000..3555615 --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/original.svg @@ -0,0 +1,27 @@ +Legacy pmatrix: spacing after x + + + diff --git a/testdata/parity/legacy-matrix-spacing/witness.d2 b/testdata/parity/legacy-matrix-spacing/witness.d2 new file mode 100644 index 0000000..83e46c3 --- /dev/null +++ b/testdata/parity/legacy-matrix-spacing/witness.d2 @@ -0,0 +1,8 @@ +grid-columns: 1 +caption: { + shape: text + label: "Legacy pmatrix: spacing after x" +} +matrix: |latex +\Huge x\pmatrix{1}y +|