authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-12 20:39:50-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-25 20:26:20-07:00
log5b4f04b30a869396c509201367a1d5b296becf7e
tree938f3851cd9eec52cf477cbebd99caf1aad15532
parentbdb38ac1f1a4e8f65b558380079eeab79033b2f8
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: render matrices, equation arrays, boxes and one-sided limits as OneNote does

A third equation-editor session (corpus/math-edit/native-editor-3) adds a matrix, an equation array, upper and lower limit objects, padded and boxed formulas, a nested fraction and a sum with only an upper limit to the MathML oracle. The model carries a matrix or array's column count (MathObject::columns, text-run data 0x0c003451, written back by the equation pass), and the mapping reproduces all ten exports: mtable cells row by row, equation-array rows with alignment groups and a mark for each ampersand, mpadded and menclose boxes, and n-ary operators that leave an empty limit out (mover or msup, munder or msub). Every object kind the editor produced now has a native check. Assisted-by: claude-fable-5.1

26 files changed, 463 insertions(+), 10 deletions(-)

corpus/math-edit/README.md+10
......@@ -25,6 +25,16 @@ degree second), a lower limit (19: `munder`, with the upright `lim` as one
2525overbar (23: `mover accent="false"` with a stretchy bar) and an accent (10:
2626`mover accent="true"` with the combining circumflex exported as `^`).
2727
28`native-editor-3/` is a third session: `\matrix(1&2@3&4)` (exported inline,
29its trailing `(\matrix(a&b@c&d))` staying literal text), `\eqarray(x&=1@y&=2)`,
30`x\above 2`, `x\below 2`, `\box(x)`, `\rect(x)`, `\underline(x)` (a literal
31`▱` before fences), `\iint x dx dy` (a bare operator), `f(x)/(x^2+1)` and
32`\sum^n x`. Its MathML adds matrices (20: `mtable` of cells row by row, the
33column count on the opening run's `0x0c003451`), equation arrays (15: rows
34with `maligngroup` and a `malignmark` for each `&`), an upper limit (33:
35`mover`), boxes (11: `mpadded`; 12: `menclose notation="box"`) and an n-ary
36operator with one empty limit (`mover`, or `msup` for integrals).
37
2838`written/candidate` is the page writer's output for
2939`equations_are_written_and_read_back`: on a fresh section, every native
3040equation of both fixtures copied as a new paragraph, then two expressions
corpus/math-edit/native-editor-3/before-read/environment.json created+7
......@@ -0,0 +1,7 @@
1{
2 "powershell": "5.1.14409.1005",
3 "schema": "xs2010",
4 "hostname": "ONE-M6-22B0E73B",
5 "cold": true,
6 "onenote": "14.0.4763.1000"
7}
corpus/math-edit/native-editor-3/before-read/hierarchy.xml created+2
......@@ -0,0 +1,2 @@
1<?xml version="1.0"?>
2<one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{87171530-19E5-4EC9-B43B-5CF46C9E566F}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-13T03:24:10.000Z" color="#B49EDE"><one:Section name="links" ID="{0DF32DF6-C20F-0C80-25AB-CBF498B1EB34}{1}{B0}" path="C:\one-tests\runs\capture\notebook\links.one" lastModifiedTime="2026-09-13T03:24:10.000Z" color="#8AA8E4"><one:Page ID="{ACFEEB7F-AA18-03BD-3300-EB94605262CF}{1}{B0}" name="Read about Rust ﷟HYPERLINK &quot;https://example.invalid/rust&quot;the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-13T03:24:10.000Z" pageLevel="1"/></one:Section></one:Notebook>
corpus/math-edit/native-editor-3/before-read/page-000.xml created+3
......@@ -0,0 +1,3 @@
1<?xml version="1.0"?>
2<one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{ACFEEB7F-AA18-03BD-3300-EB94605262CF}{1}{B0}" name="Read about Rust ﷟HYPERLINK &quot;https://example.invalid/rust&quot;the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-13T03:24:10.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="Author" lastModifiedBy="Author" lastModifiedTime="2026-09-12T09:49:16.000Z" objectID="{0DF32DF6-C20F-0C80-25AB-CBF498B1EB34}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="198.3904571533203" height="13.4277172088623"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-12T09:49:16.000Z" objectID="{0DF32DF6-C20F-0C80-25AB-CBF498B1EB34}{24}{B0}" alignment="left"><one:T><![CDATA[Read about Rust <a
3href="https://example.invalid/rust">the Rust site</a>]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page>
corpus/math-edit/native-editor-3/before-read/payloads.json created+3
......@@ -0,0 +1,3 @@
1[
2
3]
\ No newline at end of file
corpus/math-edit/native-editor-3/commands.jsonl created+6
......@@ -0,0 +1,6 @@
1{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
2{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-22B0E73B -ExpectedPages 1 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null}
3{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
4{"command": "powershell -NoProfile -Command \"Rename-Item C:\\one-tests\\runs\\capture\\read before-read\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
5{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-22B0E73B -UseCurrentCache -ExpectedPages -1", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null}
6{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
corpus/math-edit/native-editor-3/equations.ahk created+113
......@@ -0,0 +1,113 @@
1ComObject("OneNote.Application").NavigateTo("{ACFEEB7F-AA18-03BD-3300-EB94605262CF}{1}{B0}", "", false)
2WinWait("ahk_exe ONENOTE.EXE", , 30)
3WinActivate("ahk_exe ONENOTE.EXE")
4WinWaitActive("ahk_exe ONENOTE.EXE", , 30)
5WinMaximize("ahk_exe ONENOTE.EXE")
6Sleep(3000)
7CoordMode("Mouse", "Screen")
8Click(300, 140)
9Sleep(500)
10Send("{End}")
11Sleep(300)
12Send("{Enter}")
13Sleep(500)
14Send("!=")
15Sleep(600)
16SendText("\matrix(1&2@3&4)")
17Send(" ")
18Sleep(600)
19Send("{End}")
20Sleep(300)
21Send("{Enter}")
22Sleep(600)
23Send("!=")
24Sleep(600)
25SendText("(\matrix(a&b@c&d))")
26Send(" ")
27Sleep(600)
28Send("{End}")
29Sleep(300)
30Send("{Enter}")
31Sleep(600)
32Send("!=")
33Sleep(600)
34SendText("\eqarray(x&=1@y&=2)")
35Send(" ")
36Sleep(600)
37Send("{End}")
38Sleep(300)
39Send("{Enter}")
40Sleep(600)
41Send("!=")
42Sleep(600)
43SendText("x\above 2")
44Send(" ")
45Sleep(600)
46Send("{End}")
47Sleep(300)
48Send("{Enter}")
49Sleep(600)
50Send("!=")
51Sleep(600)
52SendText("x\below 2")
53Send(" ")
54Sleep(600)
55Send("{End}")
56Sleep(300)
57Send("{Enter}")
58Sleep(600)
59Send("!=")
60Sleep(600)
61SendText("\box(x)")
62Send(" ")
63Sleep(600)
64Send("{End}")
65Sleep(300)
66Send("{Enter}")
67Sleep(600)
68Send("!=")
69Sleep(600)
70SendText("\rect(x)")
71Send(" ")
72Sleep(600)
73Send("{End}")
74Sleep(300)
75Send("{Enter}")
76Sleep(600)
77Send("!=")
78Sleep(600)
79SendText("\underline(x)")
80Send(" ")
81Sleep(600)
82Send("{End}")
83Sleep(300)
84Send("{Enter}")
85Sleep(600)
86Send("!=")
87Sleep(600)
88SendText("\iint x dx dy")
89Send(" ")
90Sleep(600)
91Send("{End}")
92Sleep(300)
93Send("{Enter}")
94Sleep(600)
95Send("!=")
96Sleep(600)
97SendText("f(x)/(x^2+1)")
98Send(" ")
99Sleep(600)
100Send("{End}")
101Sleep(300)
102Send("{Enter}")
103Sleep(600)
104Send("!=")
105Sleep(600)
106SendText("\sum^n x")
107Send(" ")
108Sleep(600)
109Send("{End}")
110Sleep(300)
111Send("{Enter}")
112Sleep(600)
113Sleep(2000)
corpus/math-edit/native-editor-3/equations.json created+13
......@@ -0,0 +1,13 @@
1{
2 "exit": 0,
3 "stdout": "",
4 "stderr": "",
5 "w": 800,
6 "h": 600,
7 "error": null,
8 "win": {
9 "title": "Read about Rust the Rust site - Microsoft OneNote",
10 "class": "Framework::CFrame",
11 "dialog": false
12 }
13}
\ No newline at end of file
corpus/math-edit/native-editor-3/equations.png created
Binary files /dev/null and b/corpus/math-edit/native-editor-3/equations.png differ
corpus/math-edit/native-editor-3/machine.json created+1
......@@ -0,0 +1 @@
1{"name": "m6-22b0e73b", "hostname": "ONE-M6-22B0E73B"}
corpus/math-edit/native-editor-3/notebook/Open Notebook.onetoc2 created
Binary files /dev/null and b/corpus/math-edit/native-editor-3/notebook/Open Notebook.onetoc2 differ
corpus/math-edit/native-editor-3/notebook/links.one created
Binary files /dev/null and b/corpus/math-edit/native-editor-3/notebook/links.one differ
corpus/math-edit/native-editor-3/read/environment.json created+7
......@@ -0,0 +1,7 @@
1{
2 "powershell": "5.1.14409.1005",
3 "schema": "xs2010",
4 "hostname": "ONE-M6-22B0E73B",
5 "cold": false,
6 "onenote": "14.0.4763.1000"
7}
corpus/math-edit/native-editor-3/read/hierarchy.xml created+2
......@@ -0,0 +1,2 @@
1<?xml version="1.0"?>
2<one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{87171530-19E5-4EC9-B43B-5CF46C9E566F}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-13T03:26:14.000Z" color="#B49EDE" isCurrentlyViewed="true"><one:Section name="links" ID="{0DF32DF6-C20F-0C80-25AB-CBF498B1EB34}{1}{B0}" path="C:\one-tests\runs\capture\notebook\links.one" lastModifiedTime="2026-09-13T03:26:14.000Z" color="#8AA8E4" isCurrentlyViewed="true"><one:Page ID="{ACFEEB7F-AA18-03BD-3300-EB94605262CF}{1}{B0}" name="Read about Rust the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-13T03:26:14.000Z" pageLevel="1" isCurrentlyViewed="true"/></one:Section></one:Notebook>
corpus/math-edit/native-editor-3/read/page-000.xml created+4
......@@ -0,0 +1,4 @@
1<?xml version="1.0"?>
2<one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{ACFEEB7F-AA18-03BD-3300-EB94605262CF}{1}{B0}" name="Read about Rust the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-13T03:26:15.000Z" pageLevel="1" isCurrentlyViewed="true" lang="en-US"><one:QuickStyleDef index="0" name="p" fontColor="automatic" highlightColor="automatic" font="Calibri" fontSize="11.0" spaceBefore="0.0" spaceAfter="0.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="Author" lastModifiedBy="Author" lastModifiedTime="2026-09-13T03:26:14.000Z" objectID="{0DF32DF6-C20F-0C80-25AB-CBF498B1EB34}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="198.3904571533203" height="221.7999877929687"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-13T03:25:48.000Z" objectID="{0DF32DF6-C20F-0C80-25AB-CBF498B1EB34}{24}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[Read about Rust <a
3href="https://example.invalid/rust">the Rust site</a>]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:25:53.000Z" lastModifiedTime="2026-09-13T03:25:55.000Z" objectID="{5C249AD4-AE33-4DED-9D24-FDCE43408E3F}{12}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML"><mml:mtable><mml:mtr><mml:mtd><mml:mn>1</mml:mn></mml:mtd><mml:mtd><mml:mn>2</mml:mn></mml:mtd></mml:mtr><mml:mtr><mml:mtd><mml:mn>3</mml:mn></mml:mtd><mml:mtd><mml:mn>4</mml:mn></mml:mtd></mml:mtr></mml:mtable></mml:math><![endif]--><span
4style='font-family:Calibri' lang=en-US>(\matrix(a&amp;b@c&amp;d)) </span>]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:25:57.000Z" lastModifiedTime="2026-09-13T03:25:57.000Z" objectID="{5C249AD4-AE33-4DED-9D24-FDCE43408E3F}{19}{B0}" alignment="left" quickStyleIndex="0" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:mtable><mml:mtr><mml:mtd><mml:maligngroup/><mml:mi>x</mml:mi><mml:malignmark/><mml:mo>=</mml:mo><mml:mn>1</mml:mn></mml:mtd></mml:mtr><mml:mtr><mml:mtd><mml:maligngroup/><mml:mi>y</mml:mi><mml:malignmark/><mml:mo>=</mml:mo><mml:mn>2</mml:mn></mml:mtd></mml:mtr></mml:mtable></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:25:58.000Z" lastModifiedTime="2026-09-13T03:26:00.000Z" objectID="{5C249AD4-AE33-4DED-9D24-FDCE43408E3F}{22}{B0}" alignment="left" quickStyleIndex="0" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:mover><mml:mi>x</mml:mi><mml:mn>2</mml:mn></mml:mover></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:26:00.000Z" lastModifiedTime="2026-09-13T03:26:02.000Z" objectID="{5C249AD4-AE33-4DED-9D24-FDCE43408E3F}{25}{B0}" alignment="left" quickStyleIndex="0" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:munder><mml:mi>x</mml:mi><mml:mn>2</mml:mn></mml:munder></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:26:03.000Z" lastModifiedTime="2026-09-13T03:26:04.000Z" objectID="{5C249AD4-AE33-4DED-9D24-FDCE43408E3F}{28}{B0}" alignment="left" quickStyleIndex="0" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:mpadded><mml:mi>x</mml:mi></mml:mpadded></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:26:05.000Z" lastModifiedTime="2026-09-13T03:26:06.000Z" objectID="{5C249AD4-AE33-4DED-9D24-FDCE43408E3F}{31}{B0}" alignment="left" quickStyleIndex="0" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:menclose notation="box"><mml:mi>x</mml:mi></mml:menclose></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:26:07.000Z" lastModifiedTime="2026-09-13T03:26:08.000Z" objectID="{5C249AD4-AE33-4DED-9D24-FDCE43408E3F}{34}{B0}" alignment="left" quickStyleIndex="0" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:mo>▱</mml:mo><mml:mfenced><mml:mi>x</mml:mi></mml:mfenced></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:26:09.000Z" lastModifiedTime="2026-09-13T03:26:10.000Z" objectID="{5C249AD4-AE33-4DED-9D24-FDCE43408E3F}{37}{B0}" alignment="left" quickStyleIndex="0" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:mo>∬</mml:mo><mml:mi>x</mml:mi><mml:mo>&nbsp;</mml:mo><mml:mi>d</mml:mi><mml:mi>x</mml:mi><mml:mo>&nbsp;</mml:mo><mml:mi>d</mml:mi><mml:mi>y</mml:mi><mml:mo>&nbsp;</mml:mo></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:26:11.000Z" lastModifiedTime="2026-09-13T03:26:12.000Z" objectID="{5C249AD4-AE33-4DED-9D24-FDCE43408E3F}{40}{B0}" alignment="left" quickStyleIndex="0" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:mfrac><mml:mrow><mml:mi>f</mml:mi><mml:mfenced><mml:mi>x</mml:mi></mml:mfenced></mml:mrow><mml:mrow><mml:msup><mml:mi>x</mml:mi><mml:mn>2</mml:mn></mml:msup><mml:mo>+</mml:mo><mml:mn>1</mml:mn></mml:mrow></mml:mfrac></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:26:14.000Z" lastModifiedTime="2026-09-13T03:26:14.000Z" objectID="{5C249AD4-AE33-4DED-9D24-FDCE43408E3F}{43}{B0}" alignment="left" quickStyleIndex="0" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:mover><mml:mo stretchy="false">∑</mml:mo><mml:mi>n</mml:mi></mml:mover><mml:mrow><mml:mi>x</mml:mi><mml:mo>&nbsp;</mml:mo></mml:mrow></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:26:14.000Z" lastModifiedTime="2026-09-13T03:26:14.000Z" objectID="{5C249AD4-AE33-4DED-9D24-FDCE43408E3F}{46}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page>
corpus/math-edit/native-editor-3/read/payloads.json created+3
......@@ -0,0 +1,3 @@
1[
2
3]
\ No newline at end of file
corpus/math-edit/native-editor-3/run.json created+18
......@@ -0,0 +1,18 @@
1{
2 "notebook": "/Users/clo/dev/one/corpus/link-edit/candidate",
3 "expected_pages": 1,
4 "author": null,
5 "author_timeout_seconds": 600,
6 "inspect": false,
7 "collect_notebook": true,
8 "base": {
9 "file": "win7-office-base.qcow2",
10 "format": "qcow2",
11 "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
12 "virtual_size": 68719476736
13 },
14 "scripts": {
15 "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
16 "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
17 }
18}
corpus/math-edit/native-editor-3/scripts/cold.ps1 created+27
......@@ -0,0 +1,27 @@
1param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '')
2Set-StrictMode -Version Latest
3$ErrorActionPreference = 'Stop'
4$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
5if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
6 throw 'Choose a run directly below C:\one-tests\runs.'
7}
8if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' }
9$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
10if ($CloneHost) {
11 if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) {
12 throw 'The disposable clone hostname does not match this machine.'
13 }
14 New-Item "$key\Options\Paths" -Force | Out-Null
15 New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null
16} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
17 -not (Test-Path 'C:\one-tests\profile-original-cache')) {
18 throw 'Park the personal OneNote profile before resetting the test cache.'
19}
20$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0'
21$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root))
22if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' }
23New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null
24if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked }
25if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse }
26New-Item "$key\OpenNotebooks" | Out-Null
27New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null
corpus/math-edit/native-editor-3/scripts/read.ps1 created+142
......@@ -0,0 +1,142 @@
1param(
2 [Parameter(Mandatory=$true)][string]$Root,
3 [int]$ExpectedPages = -1,
4 [switch]$UseCurrentCache,
5 [switch]$Pdf,
6 [switch]$KeepOpen,
7 [string]$CloneHost = ''
8)
9Set-StrictMode -Version Latest
10$ErrorActionPreference = 'Stop'
11$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
12if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
13 throw 'Choose a run directly below C:\one-tests\runs.'
14}
15$notebook = Join-Path $root 'notebook'
16$output = Join-Path $root 'read'
17if (Test-Path $output) { throw 'Choose a new read destination.' }
18if ($UseCurrentCache) {
19 if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
20 throw 'Park the personal OneNote profile before reading test notebooks.'
21 }
22} else {
23 & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
24}
25New-Item -ItemType Directory -Path $output | Out-Null
26$app = New-Object -ComObject OneNote.Application
27$notebookId = ''
28$failure = $null
29try {
30 $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
31 $process = Get-Process ONENOTE
32 @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
33 powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
34 ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
35 $sections = @()
36 foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
37 $id = ''
38 $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
39 $sections += $id
40 }
41 $deadline = [DateTime]::UtcNow.AddSeconds(300)
42 $previous = ''
43 $lastChange = ''
44 $stableSince = [DateTime]::UtcNow
45 $settled = $false
46 do {
47 $pages = @{}
48 foreach ($section in $sections) {
49 $hierarchy = ''
50 $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
51 [xml]$xml = $hierarchy
52 foreach ($node in $xml.SelectNodes('//*[@path]')) {
53 if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
54 throw 'OneNote opened a section outside the copied notebook.'
55 }
56 }
57 foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
58 $id = $node.GetAttribute('ID')
59 $content = ''
60 $app.GetPageContent($id, [ref]$content, 1, 1)
61 $pages[$id] = $content
62 }
63 }
64 $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
65 if ($signature -ne $previous) {
66 $lastChange = $previous
67 $previous = $signature
68 $stableSince = [DateTime]::UtcNow
69 }
70 if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
71 ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
72 ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
73 Start-Sleep -Milliseconds 250
74 } while ([DateTime]::UtcNow -lt $deadline)
75 if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
76 [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8)
77 $index = 0
78 foreach ($id in @($pages.Keys | Sort-Object)) {
79 [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
80 $index++
81 }
82 $hierarchy = ''
83 $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
84 [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
85 throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled."
86 }
87 $index = 0
88 $payloads = @()
89 foreach ($id in @($pages.Keys | Sort-Object)) {
90 [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
91 if ($Pdf) {
92 $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
93 $app.NavigateTo($id, '', $false)
94 $app.Publish($id, $pdfPath, 3, '')
95 if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
96 }
97 [xml]$page = $pages[$id]
98 foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
99 $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
100 $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
101 [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
102 $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID');
103 kind = $file.LocalName; name = $file.GetAttribute('preferredName');
104 sha256 = $hash; bytes = $bytes.Length }
105 }
106 $index++
107 }
108 [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
109 $all = ''
110 $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
111 [xml]$finalTree = $all
112 $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
113 if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
114 throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
115 }
116 [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
117 Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
118} catch {
119 $failure = $_
120 [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
121 throw
122} finally {
123 try {
124 try {
125 if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
126 if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
127 $app.CloseNotebook($notebookId, $false)
128 }
129 } catch {
130 if ($null -eq $failure) { throw }
131 [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
132 }
133 } finally {
134 [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
135 }
136 $app = $null
137 [GC]::Collect()
138 [GC]::WaitForPendingFinalizers()
139 if (-not $UseCurrentCache -and -not $CloneHost) {
140 Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
141 }
142}
corpus/math-edit/native-editor-3/source.json created+14
......@@ -0,0 +1,14 @@
1[
2 {
3 "path": "Open Notebook.onetoc2",
4 "bytes": 1604,
5 "sha256": "6731c4214437fe1cb6aa347b0004542b7f7e4260f47773bf9ec07c4e894de47c",
6 "mtime_ns": 1789206821144296891
7 },
8 {
9 "path": "links.one",
10 "bytes": 6128,
11 "sha256": "0cfb356ed374fe919f73fb73fdbe5d66e0baa484d4119f70074d9f869d6db78a",
12 "mtime_ns": 1789206821116200533
13 }
14]
corpus/math-edit/native-editor-3/teardown.json created+1
......@@ -0,0 +1 @@
1{"absent": true}
crates/onestore/src/document.rs+3
......@@ -136,6 +136,8 @@ format_fields! {
136136pub struct MathObject {
137137 pub kind: u32,
138138 pub arguments: Option<u32>,
139 /// Column count of a matrix or equation array.
140 pub columns: Option<u8>,
139141 pub symbols: Vec<char>,
140142}
141143
......@@ -232,6 +234,7 @@ impl Revision<'_> {
232234 })
233235 })
234236 .transpose()?,
237 columns: scalar(0x0c003451).and_then(|b| b.first().copied()),
235238 symbols: [0x10003453, 0x10003454, 0x10003455]
236239 .into_iter()
237240 .filter_map(scalar)
crates/onestore/src/page/math.rs+69-10
......@@ -1,9 +1,9 @@
11//! Equations as OneNote stores them: a linear text where U+FDD0 opens an inline object,
22//! U+FDEE separates its arguments and U+FDEF closes it, with the object's kind on the run
33//! data of the opening character. `Math::parse` builds the tree and `mathml` renders it the
4//! way OneNote's own export does for the kinds native fixtures have verified (superscript,
5//! subscript, both, fraction, brackets, n-ary operators with limits); radicals, under/over
6//! limits and the rest follow the text object model's meaning without a native check.
4//! way OneNote's own export does, verified against native fixtures for every kind the
5//! equation editor produced: scripts, fractions, fences, n-ary operators with any limits,
6//! radicals, limit objects, accents, overbars, boxes, matrices and equation arrays.
77
88use super::text::Paragraph;
99use crate::document::MathObject;
......@@ -18,6 +18,9 @@ pub enum Math {
1818 Object {
1919 kind: u32,
2020 symbols: Vec<char>,
21 /// Column count of a matrix (arguments are its cells, row by row) or an equation
22 /// array (arguments are its rows).
23 columns: Option<u8>,
2124 arguments: Vec<Vec<Math>>,
2225 },
2326}
......@@ -48,6 +51,7 @@ impl Math {
4851 struct Frame {
4952 kind: u32,
5053 symbols: Vec<char>,
54 columns: Option<u8>,
5155 arguments: Vec<Vec<Math>>,
5256 }
5357 let mut frames: Vec<Frame> = Vec::new();
......@@ -83,6 +87,7 @@ impl Math {
8387 frames.push(Frame {
8488 kind: object.kind,
8589 symbols: object.symbols.clone(),
90 columns: object.columns,
8691 arguments: Vec::new(),
8792 });
8893 sequences.push(Vec::new());
......@@ -105,6 +110,7 @@ impl Math {
105110 sequences.last_mut().unwrap().push(Math::Object {
106111 kind: frame.kind,
107112 symbols: frame.symbols,
113 columns: frame.columns,
108114 arguments: frame.arguments,
109115 });
110116 }
......@@ -147,6 +153,7 @@ impl Math {
147153 Math::Object {
148154 kind,
149155 symbols,
156 columns,
150157 arguments,
151158 } => {
152159 // A lone letter, number or operator stands bare; anything else is a row.
......@@ -176,6 +183,44 @@ impl Math {
176183 out.push_str(&format!("</mml:{element}>"));
177184 };
178185 match (kind, arguments.len()) {
186 (11, 1) => wrapped(out, "mpadded", &[0]),
187 (12, 1) => {
188 out.push_str("<mml:menclose notation=\"box\">");
189 argument(out, 0);
190 out.push_str("</mml:menclose>");
191 }
192 // Matrix cells row by row; an equation array's rows carry `&` alignment
193 // marks, exported as an alignment group at the row start and a mark at each.
194 (20, _) | (15, _) => {
195 let width = if *kind == 20 {
196 usize::from(columns.unwrap_or(1).max(1))
197 } else {
198 1
199 };
200 out.push_str("<mml:mtable>");
201 for (r, row) in arguments.chunks(width).enumerate() {
202 out.push_str("<mml:mtr>");
203 for (c, cell) in row.iter().enumerate() {
204 out.push_str("<mml:mtd>");
205 if *kind == 15 {
206 out.push_str("<mml:maligngroup/>");
207 for node in cell {
208 match node {
209 Math::Operator('&') => {
210 out.push_str("<mml:malignmark/>")
211 }
212 node => node.write(out),
213 }
214 }
215 } else {
216 argument(out, r * width + c);
217 }
218 out.push_str("</mml:mtd>");
219 }
220 out.push_str("</mml:mtr>");
221 }
222 out.push_str("</mml:mtable>");
223 }
179224 (31, 2) => wrapped(out, "msup", &[0, 1]),
180225 (29, 2) => wrapped(out, "msub", &[0, 1]),
181226 (30, 3) => wrapped(out, "msubsup", &[0, 1, 2]),
......@@ -220,19 +265,29 @@ impl Math {
220265 out.push_str("</mml:mover>");
221266 }
222267 // Lower limit, upper limit, body; integrals take their limits as scripts,
223 // other operators above and below.
268 // other operators above and below, and an empty limit leaves its side out.
224269 (21, 3) => {
225270 let operator = symbols.first().copied().unwrap_or('∑');
226 let element = if ('\u{222b}'..='\u{2233}').contains(&operator) {
227 "msubsup"
228 } else {
229 "munderover"
271 let scripts = ('\u{222b}'..='\u{2233}').contains(&operator);
272 let (lower, upper) = (!arguments[0].is_empty(), !arguments[1].is_empty());
273 let element = match (lower, upper, scripts) {
274 (true, true, true) => "msubsup",
275 (true, true, false) => "munderover",
276 (true, false, true) => "msub",
277 (true, false, false) => "munder",
278 (false, true, true) => "msup",
279 (false, true, false) => "mover",
280 (false, false, _) => "mrow",
230281 };
231282 out.push_str(&format!(
232283 "<mml:{element}><mml:mo stretchy=\"false\">{operator}</mml:mo>"
233284 ));
234 argument(out, 0);
235 argument(out, 1);
285 if lower {
286 argument(out, 0);
287 }
288 if upper {
289 argument(out, 1);
290 }
236291 out.push_str(&format!("</mml:{element}>"));
237292 out.push_str("<mml:mrow>");
238293 for node in &arguments[2] {
......@@ -320,6 +375,7 @@ impl Math {
320375 format.math_object = Some(object.unwrap_or(MathObject {
321376 kind: PLAIN_RUN,
322377 arguments: None,
378 columns: None,
323379 symbols: Vec::new(),
324380 }));
325381 (text, format)
......@@ -341,6 +397,7 @@ fn write_sequence(nodes: &[Math], runs: &mut Vec<(String, Option<MathObject>)>)
341397 Math::Object {
342398 kind,
343399 symbols,
400 columns,
344401 arguments,
345402 } => {
346403 if !leaf.is_empty() {
......@@ -351,6 +408,7 @@ fn write_sequence(nodes: &[Math], runs: &mut Vec<(String, Option<MathObject>)>)
351408 Some(MathObject {
352409 kind: *kind,
353410 arguments: Some(arguments.len() as u32),
411 columns: *columns,
354412 symbols: symbols.clone(),
355413 }),
356414 ));
......@@ -365,6 +423,7 @@ fn write_sequence(nodes: &[Math], runs: &mut Vec<(String, Option<MathObject>)>)
365423 let object = MathObject {
366424 kind: *kind,
367425 arguments: (index > 0).then_some(index as u32),
426 columns: None,
368427 symbols: Vec::new(),
369428 };
370429 let extended = runs.len() > before;
crates/onestore/src/page/write.rs+3
......@@ -1770,6 +1770,9 @@ impl Lowering<'_> {
17701770 if let Some(count) = object.arguments {
17711771 set.push((0x14003450, count.to_le_bytes().to_vec()));
17721772 }
1773 if let Some(columns) = object.columns {
1774 set.push((0x0c003451, vec![columns]));
1775 }
17731776 for (id, symbol) in [0x10003453, 0x10003454, 0x10003455]
17741777 .into_iter()
17751778 .zip(&object.symbols)
crates/onestore/tests/page_math.rs+11
......@@ -18,6 +18,12 @@ const EDITOR_2: &[u8] =
1818 include_bytes!("../../../corpus/math-edit/native-editor-2/notebook/links.one");
1919const EDITOR_2_READ: &str =
2020 include_str!("../../../corpus/math-edit/native-editor-2/read/page-000.xml");
21/// A third session: a matrix, an equation array, upper and lower limit objects, boxes, a
22/// nested fraction and a sum with only an upper limit.
23const EDITOR_3: &[u8] =
24 include_bytes!("../../../corpus/math-edit/native-editor-3/notebook/links.one");
25const EDITOR_3_READ: &str =
26 include_str!("../../../corpus/math-edit/native-editor-3/read/page-000.xml");
2127
2228fn native_mathml(read: &str) -> Vec<String> {
2329 read.split("<mml:math")
......@@ -101,12 +107,17 @@ fn equation_editor_expressions_render_to_the_mathml_onenote_exports() {
101107 rendered_equations(EDITOR_2, "Read about Rust the Rust site"),
102108 native_mathml(EDITOR_2_READ)
103109 );
110 assert_eq!(
111 rendered_equations(EDITOR_3, "Read about Rust the Rust site"),
112 native_mathml(EDITOR_3_READ)
113 );
104114}
105115
106116fn object(kind: u32, symbols: &str, arguments: Vec<Vec<Math>>) -> Math {
107117 Math::Object {
108118 kind,
109119 symbols: symbols.chars().collect(),
120 columns: None,
110121 arguments,
111122 }
112123}
tools/test_math_edit.py+1
......@@ -23,6 +23,7 @@ class MathEditTest(unittest.TestCase):
2323 def test_equation_editor_expressions_export_as_mathml(self):
2424 self.assertEqual(len(exported_mathml(FIXTURE / 'native-editor/read/page-000.xml')), 5)
2525 self.assertEqual(len(exported_mathml(FIXTURE / 'native-editor-2/read/page-000.xml')), 9)
26 self.assertEqual(len(exported_mathml(FIXTURE / 'native-editor-3/read/page-000.xml')), 10)
2627
2728 def test_rust_written_equations_export_the_expected_mathml(self):
2829 with TemporaryDirectory() as temporary: