authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-12 20:22:19-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-25 20:26:20-07:00
logbdb38ac1f1a4e8f65b558380079eeab79033b2f8
treeef5b5e7b1c3bb3f77e990989a97b878260422867
parent6478bd1e6e851e3c9bfe58640dfb03ffb64f09f8
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: render roots, limits, named fences, accents and function names as OneNote does

A second equation-editor session (corpus/math-edit/native-editor-2, typed by tools/native_math.py with ONESTORE_MATH_EQUATIONS) adds square and n-th roots, a plain fraction, a limit with an upright function name, a product, square brackets, an overbar and a hat to the MathML oracle, and the mapping now reproduces all nine exports byte for byte: radicals render as msqrt or mroot with the degree second, brackets other than parentheses name their fences, an accent object is mover accent="true" with the combining mark exported as its spacing character, an overbar is mover accent="false" with a stretchy bar, and runs of upright letters are one function-name identifier that a limit base wraps in a row. Mouse ink drawn over an outline in OneNote 2010 becomes a page-level drawing (corpus/ink-edit/native-mouse-over-text, tools/native_handwriting.py), so handwriting inside text runs cannot be authored in the lab and stays read-only retained content. Assisted-by: claude-fable-5.1

47 files changed, 834 insertions(+), 4 deletions(-)

corpus/ink-edit/README.md+6
...@@ -24,3 +24,9 @@ outline grown to contain the strokes' absolute page coordinates....@@ -24,3 +24,9 @@ outline grown to contain the strokes' absolute page coordinates.
24directories while running the tests, then cold-open each with24directories while running the tests, then cold-open each with
25`tools/native_runner.py OUTPUT COLD --expected-pages 1 --collect-notebook25`tools/native_runner.py OUTPUT COLD --expected-pages 1 --collect-notebook
26--screenshots`.26--screenshots`.
27
28`native-mouse-over-text/` is OneNote 2010 receiving mouse strokes over an
29outline's text and in empty space (`tools/native_handwriting.py`): both
30became page-level `InkDrawing` objects. Mouse ink never produces
31handwriting paragraphs or embedded ink in text runs, so those forms stay
32read-only retained content until a pen-authored fixture exists.
corpus/ink-edit/native-mouse-over-text/before-read/environment.json created+7
...@@ -0,0 +1,7 @@
1{
2 "powershell": "5.1.14409.1005",
3 "schema": "xs2010",
4 "hostname": "ONE-M6-9C262230",
5 "cold": true,
6 "onenote": "14.0.4763.1000"
7}
corpus/ink-edit/native-mouse-over-text/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="{81D3E2FA-1372-4C5B-9A6E-5DBF99B76CED}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-13T03:08:22.000Z" color="#B49EDE"><one:Section name="links" ID="{13A54437-B3B6-01B2-29B8-E2E4F68FD99E}{1}{B0}" path="C:\one-tests\runs\capture\notebook\links.one" lastModifiedTime="2026-09-13T03:08:22.000Z" color="#8AA8E4"><one:Page ID="{B2A882BE-DBA1-0E8F-3F13-C2840E6C5065}{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:08:22.000Z" pageLevel="1"/></one:Section></one:Notebook>
corpus/ink-edit/native-mouse-over-text/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="{B2A882BE-DBA1-0E8F-3F13-C2840E6C5065}{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:08:22.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="{13A54437-B3B6-01B2-29B8-E2E4F68FD99E}{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="{13A54437-B3B6-01B2-29B8-E2E4F68FD99E}{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/ink-edit/native-mouse-over-text/before-read/payloads.json created+3
...@@ -0,0 +1,3 @@
1[
2
3]
\ No newline at end of file
corpus/ink-edit/native-mouse-over-text/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-9C262230 -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-9C262230 -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/ink-edit/native-mouse-over-text/handwriting.ahk created+34
...@@ -0,0 +1,34 @@
1ComObject("OneNote.Application").NavigateTo("{B2A882BE-DBA1-0E8F-3F13-C2840E6C5065}{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)
7SendMode("Event")
8CoordMode("Mouse", "Screen")
9Click(258,36)
10Sleep(600)
11Click(205,69)
12Sleep(600)
13; handwriting inside the outline, below its text line
14MouseMove(140,160)
15Click("Down")
16MouseMove(160,175,25)
17MouseMove(180,160,25)
18MouseMove(200,175,25)
19MouseMove(220,160,25)
20Click("Up")
21Sleep(300)
22MouseClickDrag("Left",240,158,300,178,25)
23Sleep(300)
24; a drawing in empty page space
25MouseMove(450,320)
26Click("Down")
27MouseMove(500,360,25)
28MouseMove(450,400,25)
29MouseMove(400,360,25)
30MouseMove(450,320,25)
31Click("Up")
32Sleep(1000)
33Click(21,74)
34Sleep(2000)
corpus/ink-edit/native-mouse-over-text/handwriting.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/ink-edit/native-mouse-over-text/handwriting.png created
Binary files /dev/null and b/corpus/ink-edit/native-mouse-over-text/handwriting.png differ
corpus/ink-edit/native-mouse-over-text/machine.json created+1
...@@ -0,0 +1 @@
1{"name": "m6-9c262230", "hostname": "ONE-M6-9C262230"}
corpus/ink-edit/native-mouse-over-text/notebook/Open Notebook.onetoc2 created
Binary files /dev/null and b/corpus/ink-edit/native-mouse-over-text/notebook/Open Notebook.onetoc2 differ
corpus/ink-edit/native-mouse-over-text/notebook/links.one created
Binary files /dev/null and b/corpus/ink-edit/native-mouse-over-text/notebook/links.one differ
corpus/ink-edit/native-mouse-over-text/read/environment.json created+7
...@@ -0,0 +1,7 @@
1{
2 "powershell": "5.1.14409.1005",
3 "schema": "xs2010",
4 "hostname": "ONE-M6-9C262230",
5 "cold": false,
6 "onenote": "14.0.4763.1000"
7}
corpus/ink-edit/native-mouse-over-text/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="{81D3E2FA-1372-4C5B-9A6E-5DBF99B76CED}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-13T03:10:25.000Z" color="#B49EDE" isCurrentlyViewed="true"><one:Section name="links" ID="{13A54437-B3B6-01B2-29B8-E2E4F68FD99E}{1}{B0}" path="C:\one-tests\runs\capture\notebook\links.one" lastModifiedTime="2026-09-13T03:10:25.000Z" color="#8AA8E4" isCurrentlyViewed="true"><one:Page ID="{B2A882BE-DBA1-0E8F-3F13-C2840E6C5065}{1}{B0}" name="Read about Rust the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-13T03:10:18.000Z" pageLevel="1" isCurrentlyViewed="true"/></one:Section></one:Notebook>
corpus/ink-edit/native-mouse-over-text/read/page-000.xml created+22
...@@ -0,0 +1,22 @@
1<?xml version="1.0"?>
2<one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{B2A882BE-DBA1-0E8F-3F13-C2840E6C5065}{1}{B0}" name="Read about Rust the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-13T03:10:18.000Z" pageLevel="1" isCurrentlyViewed="true" 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-13T03:10:03.000Z" objectID="{13A54437-B3B6-01B2-29B8-E2E4F68FD99E}{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-13T03:10:03.000Z" objectID="{13A54437-B3B6-01B2-29B8-E2E4F68FD99E}{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:InkDrawing lastModifiedTime="2026-09-13T03:10:14.000Z" objectID="{880F602E-E250-42FE-BA4D-D86F4AAE4E9E}{16}{B0}"><one:Position x="144.0" y="56.2393684387207" z="1"/><one:Size width="45.01417922973633" height="15.05196857452393"/><one:Data>ANoCHQJ6KgFgKgAaH4tCcEWEdYC+0EXZBPqHlTLy7BdBuMAWv/Mr9HreuxOL5V6zSJyboSVat1ES
4v+xxzMwisUGJDrsTLO56oVjPVIrml8VPjwb4utLhmyLEZm2nDbJYSZodTkA0BmwQAwtIFETtuZAB
5RSNGIwUIC2VPZmdoZGkZFDIIAMAMAgAAyEEzCACwCQIAAMhBZAMVRgBACr4BSIfwsXeFcUmUzTWa
6EzmU1mk1gEBmkzgMAgKAQGATWZQCAzOaQGZzSAzOaTWZTOZTNNJrMpmmk1mUzTSazKZppNUymc0m
7qZTOZTOH7f7t/5pAZnNIDM5pAZnNJrMpnMpmmk1mUzTSaplMyaTVMpmTSaplMyaTVMpmmk1AAAAF
8FEYAQAAACQQFFEYAAAAAEgARILeLLBPpguVJo6ME9ct7COwKP0AjYQMhZDLQO6ADFUYAQAo/QCKS
9wgP7+zf4wA==</one:Data></one:InkDrawing><one:InkDrawing lastModifiedTime="2026-09-13T03:10:18.000Z" objectID="{880F602E-E250-42FE-BA4D-D86F4AAE4E9E}{21}{B0}"><one:Position x="263.9905395507812" y="177.7606201171875" z="2"/><one:Size width="75.0330810546875" height="60.00945281982422"/><one:Data>AOMFHQTKAaIBAWAqABofi0JwRYR1gL7QRdkE+oeVMvLsF0G4wBa/8yv0et67E4vlXrNInJuhJVq3
10URK/7HHMzCKxQYkOuxMs7nqhWM9UiuaXxU+PBvi60uGbIsRmbacNslhJmh1OQDQGbBADC0gURO25
11kAFFI0YjBQgLZU9mZ2hkaRkUMggAwAwCAADIQTMIALAJAgAAyEFkAxVGAEAKxQSNAofyDLeQS2Zz
12KAwCZzKazSZzCYwGAQGAQGAQGAQGAQFM5pAZnNIDM5pNZlM5pNZlM5pNUymcymaaTWZTNNJqmUzm
13k1TKZzSaplM7SIBMJnNJrMpnNEBgEBgEBgEBgEBgEBmkzTKawCZTWaTOZTWaTOZTWaJnMprNEzmU
14zmSazRM5lNZomcyms0FpIBAZlM5pNZlMZhAIDAIDAIDAIDAIDAEymsAmU1gEyms0mcyms0mcyms0
15TOZTOZJrNJnMk1miZzKazRM5lNZoLRM5kgMxmU1mkzmU1QCAwCAwCAwCAwCAwCazJM5pAZnNJrMp
16nNJrMpnNJqmUzmk1TKZzKZppNUymc0mqZTOaTVMpmIfw++eH0maTWaTOAwCZTWAQGAQFAIDM5pAY
17BNZlAJrMoBNZlM5lM5pNUymc0mqZTMmk1TKZppNSZTNNJqTKZplM00gM0mcymsAgM0msAgMAgMAg
18MAmsygEBmc0gMzmUAmsymc0msymaaTWZTNNJqTKZppNUymZMpmmk1JlM00mqZYdTSazSAwCazKAQ
19GAIDAIDNJnAYBMprAJlNYBMprNJnMpnMk1mkzmSazQmcyTWaJnMiazRM5kTWaJnMi0ppNZlAIDM5
20ogMAgMAgMAmU1gEBmkzTKawCZTWaTOZTWaJnMprNEzmRNZomcyTWaEzmSZzIms0TOZAFFEYAgAAA
21CQQFFEYAAAAAEgARIHXkv4XbUV9Btx3j0SO9N8YKABEgsI0hZS1D3QEDFUYAQAoAESDcOupAw5Sd
22QA==</one:Data></one:InkDrawing></one:Page>
corpus/ink-edit/native-mouse-over-text/read/payloads.json created+3
...@@ -0,0 +1,3 @@
1[
2
3]
\ No newline at end of file
corpus/ink-edit/native-mouse-over-text/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/ink-edit/native-mouse-over-text/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/ink-edit/native-mouse-over-text/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/ink-edit/native-mouse-over-text/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/ink-edit/native-mouse-over-text/teardown.json created+1
...@@ -0,0 +1 @@
1{"absent": true}
corpus/math-edit/README.md+10
...@@ -15,6 +15,16 @@ the stored section and compares it with the exported MathML byte for byte;...@@ -15,6 +15,16 @@ the stored section and compares it with the exported MathML byte for byte;
15together with `corpus/m6/native-math-01` (superscripts, a fraction, inline15together with `corpus/m6/native-math-01` (superscripts, a fraction, inline
16Greek) that covers the kinds `onestore::page::Math` claims.16Greek) that covers the kinds `onestore::page::Math` claims.
1717
18`native-editor-2/` is a second session (`ONESTORE_MATH_EQUATIONS` on
19`tools/native_math.py`): `\sqrt x+1`, `\cbrt(x)`, `\sqrt(n&x)`, `a/b`,
20`\lim_(x\to 0) f(x)` (the `\lim` stayed literal), `\prod_(k=1)^n k`,
21`[a+b]`, `\overline(x)` and `x\hat`; `\sqrt (x+1)` again left an empty
22paragraph. Its MathML adds radicals (25: `msqrt`, or `mroot` with the
23degree second), a lower limit (19: `munder`, with the upright `lim` as one
24`mi` in a row), a product, named fences (`mfenced open="[" close="]"`), an
25overbar (23: `mover accent="false"` with a stretchy bar) and an accent (10:
26`mover accent="true"` with the combining circumflex exported as `^`).
27
18`written/candidate` is the page writer's output for28`written/candidate` is the page writer's output for
19`equations_are_written_and_read_back`: on a fresh section, every native29`equations_are_written_and_read_back`: on a fresh section, every native
20equation of both fixtures copied as a new paragraph, then two expressions30equation of both fixtures copied as a new paragraph, then two expressions
corpus/math-edit/native-editor-2/before-read/environment.json created+7
...@@ -0,0 +1,7 @@
1{
2 "powershell": "5.1.14409.1005",
3 "schema": "xs2010",
4 "hostname": "ONE-M6-C31DFB94",
5 "cold": true,
6 "onenote": "14.0.4763.1000"
7}
corpus/math-edit/native-editor-2/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="{92C524E4-323B-4FB6-A579-63EE80B74012}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-13T03:07:43.000Z" color="#B49EDE"><one:Section name="links" ID="{894B3830-739E-0935-36CF-A0E48027D776}{1}{B0}" path="C:\one-tests\runs\capture\notebook\links.one" lastModifiedTime="2026-09-13T03:07:43.000Z" color="#8AA8E4"><one:Page ID="{2846FEB9-1B89-0608-2064-808478C45E8D}{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:07:43.000Z" pageLevel="1"/></one:Section></one:Notebook>
corpus/math-edit/native-editor-2/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="{2846FEB9-1B89-0608-2064-808478C45E8D}{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:07:43.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="{894B3830-739E-0935-36CF-A0E48027D776}{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="{894B3830-739E-0935-36CF-A0E48027D776}{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-2/before-read/payloads.json created+3
...@@ -0,0 +1,3 @@
1[
2
3]
\ No newline at end of file
corpus/math-edit/native-editor-2/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-C31DFB94 -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-C31DFB94 -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-2/equations.ahk created+104
...@@ -0,0 +1,104 @@
1ComObject("OneNote.Application").NavigateTo("{2846FEB9-1B89-0608-2064-808478C45E8D}{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("\sqrt x+1")
17Send(" ")
18Sleep(600)
19Send("{End}")
20Sleep(300)
21Send("{Enter}")
22Sleep(600)
23Send("!=")
24Sleep(600)
25SendText("\sqrt (x+1)")
26Send(" ")
27Sleep(600)
28Send("{End}")
29Sleep(300)
30Send("{Enter}")
31Sleep(600)
32Send("!=")
33Sleep(600)
34SendText("\cbrt(x)")
35Send(" ")
36Sleep(600)
37Send("{End}")
38Sleep(300)
39Send("{Enter}")
40Sleep(600)
41Send("!=")
42Sleep(600)
43SendText("\sqrt(n&x)")
44Send(" ")
45Sleep(600)
46Send("{End}")
47Sleep(300)
48Send("{Enter}")
49Sleep(600)
50Send("!=")
51Sleep(600)
52SendText("a/b")
53Send(" ")
54Sleep(600)
55Send("{End}")
56Sleep(300)
57Send("{Enter}")
58Sleep(600)
59Send("!=")
60Sleep(600)
61SendText("\lim_(x\to 0) f(x)")
62Send(" ")
63Sleep(600)
64Send("{End}")
65Sleep(300)
66Send("{Enter}")
67Sleep(600)
68Send("!=")
69Sleep(600)
70SendText("\prod_(k=1)^n k")
71Send(" ")
72Sleep(600)
73Send("{End}")
74Sleep(300)
75Send("{Enter}")
76Sleep(600)
77Send("!=")
78Sleep(600)
79SendText("[a+b]")
80Send(" ")
81Sleep(600)
82Send("{End}")
83Sleep(300)
84Send("{Enter}")
85Sleep(600)
86Send("!=")
87Sleep(600)
88SendText("\overline(x)")
89Send(" ")
90Sleep(600)
91Send("{End}")
92Sleep(300)
93Send("{Enter}")
94Sleep(600)
95Send("!=")
96Sleep(600)
97SendText("x\hat")
98Send(" ")
99Sleep(600)
100Send("{End}")
101Sleep(300)
102Send("{Enter}")
103Sleep(600)
104Sleep(2000)
corpus/math-edit/native-editor-2/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-2/equations.png created
Binary files /dev/null and b/corpus/math-edit/native-editor-2/equations.png differ
corpus/math-edit/native-editor-2/machine.json created+1
...@@ -0,0 +1 @@
1{"name": "m6-c31dfb94", "hostname": "ONE-M6-C31DFB94"}
corpus/math-edit/native-editor-2/notebook/Open Notebook.onetoc2 created
Binary files /dev/null and b/corpus/math-edit/native-editor-2/notebook/Open Notebook.onetoc2 differ
corpus/math-edit/native-editor-2/notebook/links.one created
Binary files /dev/null and b/corpus/math-edit/native-editor-2/notebook/links.one differ
corpus/math-edit/native-editor-2/read/environment.json created+7
...@@ -0,0 +1,7 @@
1{
2 "powershell": "5.1.14409.1005",
3 "schema": "xs2010",
4 "hostname": "ONE-M6-C31DFB94",
5 "cold": false,
6 "onenote": "14.0.4763.1000"
7}
corpus/math-edit/native-editor-2/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="{92C524E4-323B-4FB6-A579-63EE80B74012}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-13T03:09:47.000Z" color="#B49EDE" isCurrentlyViewed="true"><one:Section name="links" ID="{894B3830-739E-0935-36CF-A0E48027D776}{1}{B0}" path="C:\one-tests\runs\capture\notebook\links.one" lastModifiedTime="2026-09-13T03:09:47.000Z" color="#8AA8E4" isCurrentlyViewed="true"><one:Page ID="{2846FEB9-1B89-0608-2064-808478C45E8D}{1}{B0}" name="Read about Rust the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-13T03:09:47.000Z" pageLevel="1" isCurrentlyViewed="true"/></one:Section></one:Notebook>
corpus/math-edit/native-editor-2/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="{2846FEB9-1B89-0608-2064-808478C45E8D}{1}{B0}" name="Read about Rust the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-13T03:09:47.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:09:47.000Z" objectID="{894B3830-739E-0935-36CF-A0E48027D776}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="198.3904571533203" height="194.8564453125"/><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:09:22.000Z" objectID="{894B3830-739E-0935-36CF-A0E48027D776}{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:09:27.000Z" lastModifiedTime="2026-09-13T03:09:28.000Z" objectID="{57D6F377-4FCE-4238-B8CA-B4984E7AF8F2}{12}{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:msqrt><mml:mi>x</mml:mi></mml:msqrt><mml:mo>+</mml:mo><mml:mn>1</mml:mn><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:09:28.000Z" lastModifiedTime="2026-09-13T03:09:28.000Z" objectID="{57D6F377-4FCE-4238-B8CA-B4984E7AF8F2}{18}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:09:31.000Z" lastModifiedTime="2026-09-13T03:09:32.000Z" objectID="{57D6F377-4FCE-4238-B8CA-B4984E7AF8F2}{21}{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:mroot><mml:mi>x</mml:mi><mml:mn>3</mml:mn></mml:mroot></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:09:33.000Z" lastModifiedTime="2026-09-13T03:09:34.000Z" objectID="{57D6F377-4FCE-4238-B8CA-B4984E7AF8F2}{24}{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:mroot><mml:mi>x</mml:mi><mml:mi>n</mml:mi></mml:mroot></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:09:34.000Z" lastModifiedTime="2026-09-13T03:09:36.000Z" objectID="{57D6F377-4FCE-4238-B8CA-B4984E7AF8F2}{27}{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:mi>a</mml:mi><mml:mi>b</mml:mi></mml:mfrac></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:09:37.000Z" lastModifiedTime="2026-09-13T03:09:38.000Z" objectID="{57D6F377-4FCE-4238-B8CA-B4984E7AF8F2}{30}{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:munder><mml:mrow><mml:mi>lim</mml:mi></mml:mrow><mml:mrow><mml:mi>x</mml:mi><mml:mo>→</mml:mo><mml:mn>0</mml:mn></mml:mrow></mml:munder><mml:mi>f</mml:mi><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:09:39.000Z" lastModifiedTime="2026-09-13T03:09:40.000Z" objectID="{57D6F377-4FCE-4238-B8CA-B4984E7AF8F2}{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:munderover><mml:mo stretchy="false">∏</mml:mo><mml:mrow><mml:mi>k</mml:mi><mml:mo>=</mml:mo><mml:mn>1</mml:mn></mml:mrow><mml:mi>n</mml:mi></mml:munderover><mml:mrow><mml:mi>k</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:09:41.000Z" lastModifiedTime="2026-09-13T03:09:42.000Z" objectID="{57D6F377-4FCE-4238-B8CA-B4984E7AF8F2}{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:mfenced open="[" close="]"><mml:mrow><mml:mi>a</mml:mi><mml:mo>+</mml:mo><mml:mi>b</mml:mi></mml:mrow></mml:mfenced></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:09:44.000Z" lastModifiedTime="2026-09-13T03:09:44.000Z" objectID="{57D6F377-4FCE-4238-B8CA-B4984E7AF8F2}{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:mover accent="false"><mml:mi>x</mml:mi><mml:mo stretchy="true">¯</mml:mo></mml:mover></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:09:45.000Z" lastModifiedTime="2026-09-13T03:09:47.000Z" objectID="{57D6F377-4FCE-4238-B8CA-B4984E7AF8F2}{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 accent="true"><mml:mi>x</mml:mi><mml:mo>^</mml:mo></mml:mover></mml:math><![endif]-->]]></one:T></one:OE><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-13T03:09:47.000Z" lastModifiedTime="2026-09-13T03:09:47.000Z" objectID="{57D6F377-4FCE-4238-B8CA-B4984E7AF8F2}{46}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page>
corpus/math-edit/native-editor-2/read/payloads.json created+3
...@@ -0,0 +1,3 @@
1[
2
3]
\ No newline at end of file
corpus/math-edit/native-editor-2/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-2/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-2/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-2/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-2/teardown.json created+1
...@@ -0,0 +1 @@
1{"absent": true}
crates/onestore/src/page/math.rs+76-4
...@@ -11,6 +11,8 @@ use crate::document::MathObject;...@@ -11,6 +11,8 @@ use crate::document::MathObject;
11#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]11#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
12pub enum Math {12pub enum Math {
13 Identifier(char),13 Identifier(char),
14 /// Upright letters, as the editor stores a function name such as `lim`.
15 Function(String),
14 Number(String),16 Number(String),
15 Operator(char),17 Operator(char),
16 Object {18 Object {
...@@ -51,6 +53,7 @@ impl Math {...@@ -51,6 +53,7 @@ impl Math {
51 let mut frames: Vec<Frame> = Vec::new();53 let mut frames: Vec<Frame> = Vec::new();
52 let mut sequences: Vec<Vec<Math>> = vec![Vec::new()];54 let mut sequences: Vec<Vec<Math>> = vec![Vec::new()];
53 let mut number = String::new();55 let mut number = String::new();
56 let mut word = String::new();
54 let mut offset = 0;57 let mut offset = 0;
55 for character in paragraph.text().chars() {58 for character in paragraph.text().chars() {
56 let here = offset;59 let here = offset;
...@@ -63,6 +66,14 @@ impl Math {...@@ -63,6 +66,14 @@ impl Math {
63 let number = std::mem::take(&mut number);66 let number = std::mem::take(&mut number);
64 sequences.last_mut().unwrap().push(Math::Number(number));67 sequences.last_mut().unwrap().push(Math::Number(number));
65 }68 }
69 if character.is_ascii_alphabetic() {
70 word.push(character);
71 continue;
72 }
73 if !word.is_empty() {
74 let word = std::mem::take(&mut word);
75 sequences.last_mut().unwrap().push(Math::Function(word));
76 }
66 match character {77 match character {
67 OBJECT_START => {78 OBJECT_START => {
68 // The span lookup treats a boundary offset as the span ending there, so the79 // The span lookup treats a boundary offset as the span ending there, so the
...@@ -108,6 +119,9 @@ impl Math {...@@ -108,6 +119,9 @@ impl Math {
108 if !number.is_empty() {119 if !number.is_empty() {
109 sequences.last_mut().unwrap().push(Math::Number(number));120 sequences.last_mut().unwrap().push(Math::Number(number));
110 }121 }
122 if !word.is_empty() {
123 sequences.last_mut().unwrap().push(Math::Function(word));
124 }
111 if !frames.is_empty() {125 if !frames.is_empty() {
112 return Err(invalid("An equation object never ends"));126 return Err(invalid("An equation object never ends"));
113 }127 }
...@@ -127,6 +141,7 @@ impl Math {...@@ -127,6 +141,7 @@ impl Math {
127 fn write(&self, out: &mut String) {141 fn write(&self, out: &mut String) {
128 match self {142 match self {
129 Math::Identifier(c) => tag(out, "mi", &c.to_string()),143 Math::Identifier(c) => tag(out, "mi", &c.to_string()),
144 Math::Function(name) => tag(out, "mi", name),
130 Math::Number(n) => tag(out, "mn", n),145 Math::Number(n) => tag(out, "mn", n),
131 Math::Operator(c) => tag(out, "mo", &c.to_string()),146 Math::Operator(c) => tag(out, "mo", &c.to_string()),
132 Math::Object {147 Math::Object {
...@@ -134,12 +149,16 @@ impl Math {...@@ -134,12 +149,16 @@ impl Math {
134 symbols,149 symbols,
135 arguments,150 arguments,
136 } => {151 } => {
137 // A lone identifier, number or operator stands bare; anything else is a row.152 // A lone letter, number or operator stands bare; anything else is a row.
138 let argument = |out: &mut String, index: usize| match arguments153 let argument = |out: &mut String, index: usize| match arguments
139 .get(index)154 .get(index)
140 .map(Vec::as_slice)155 .map(Vec::as_slice)
141 {156 {
142 Some([single]) if !matches!(single, Math::Object { .. }) => single.write(out),157 Some([single])
158 if !matches!(single, Math::Object { .. } | Math::Function(_)) =>
159 {
160 single.write(out)
161 }
143 Some(many) => {162 Some(many) => {
144 out.push_str("<mml:mrow>");163 out.push_str("<mml:mrow>");
145 for node in many {164 for node in many {
...@@ -165,8 +184,41 @@ impl Math {...@@ -165,8 +184,41 @@ impl Math {
165 (25, _) => wrapped(out, "msqrt", &[arguments.len() - 1]),184 (25, _) => wrapped(out, "msqrt", &[arguments.len() - 1]),
166 (19, 2) => wrapped(out, "munder", &[0, 1]),185 (19, 2) => wrapped(out, "munder", &[0, 1]),
167 (33, 2) => wrapped(out, "mover", &[0, 1]),186 (33, 2) => wrapped(out, "mover", &[0, 1]),
168 // OneNote exports parentheses as default fences, whatever the pair.187 // Parentheses are the default fences; other pairs name themselves.
169 (13, _) => wrapped(out, "mfenced", &[0]),188 (13, _) => {
189 let (open, close) = (symbols.first(), symbols.get(1));
190 if open == Some(&'(') && close == Some(&')') {
191 wrapped(out, "mfenced", &[0]);
192 } else {
193 out.push_str("<mml:mfenced");
194 if let Some(open) = open {
195 out.push_str(&format!(" open=\"{}\"", escaped(*open)));
196 }
197 if let Some(close) = close {
198 out.push_str(&format!(" close=\"{}\"", escaped(*close)));
199 }
200 out.push('>');
201 argument(out, 0);
202 out.push_str("</mml:mfenced>");
203 }
204 }
205 (10, 1) | (23, 1) => {
206 let accent = *kind == 10;
207 out.push_str(&format!("<mml:mover accent=\"{accent}\">"));
208 argument(out, 0);
209 if accent {
210 tag(
211 out,
212 "mo",
213 &spacing(symbols.first().copied().unwrap_or('^')).to_string(),
214 );
215 } else {
216 out.push_str("<mml:mo stretchy=\"true\">");
217 out.push(symbols.first().copied().unwrap_or('¯'));
218 out.push_str("</mml:mo>");
219 }
220 out.push_str("</mml:mover>");
221 }
170 // Lower limit, upper limit, body; integrals take their limits as scripts,222 // Lower limit, upper limit, body; integrals take their limits as scripts,
171 // other operators above and below.223 // other operators above and below.
172 (21, 3) => {224 (21, 3) => {
...@@ -204,6 +256,25 @@ impl Math {...@@ -204,6 +256,25 @@ impl Math {
204 }256 }
205}257}
206258
259fn escaped(c: char) -> String {
260 match c {
261 '<' => "&lt;".into(),
262 '&' => "&amp;".into(),
263 '"' => "&quot;".into(),
264 c => c.to_string(),
265 }
266}
267
268/// The spacing character OneNote exports for a combining accent.
269fn spacing(c: char) -> char {
270 match c {
271 '\u{302}' => '^',
272 '\u{303}' => '~',
273 '\u{308}' => '¨',
274 c => c,
275 }
276}
277
207fn tag(out: &mut String, element: &str, content: &str) {278fn tag(out: &mut String, element: &str, content: &str) {
208 out.push_str(&format!("<mml:{element}>"));279 out.push_str(&format!("<mml:{element}>"));
209 for c in content.chars() {280 for c in content.chars() {
...@@ -264,6 +335,7 @@ fn write_sequence(nodes: &[Math], runs: &mut Vec<(String, Option<MathObject>)>)...@@ -264,6 +335,7 @@ fn write_sequence(nodes: &[Math], runs: &mut Vec<(String, Option<MathObject>)>)
264 for node in nodes {335 for node in nodes {
265 match node {336 match node {
266 Math::Identifier(c) => leaf.push(italic(*c)),337 Math::Identifier(c) => leaf.push(italic(*c)),
338 Math::Function(name) => leaf.push_str(name),
267 Math::Number(n) => leaf.push_str(n),339 Math::Number(n) => leaf.push_str(n),
268 Math::Operator(c) => leaf.push(*c),340 Math::Operator(c) => leaf.push(*c),
269 Math::Object {341 Math::Object {
crates/onestore/tests/page_math.rs+10
...@@ -12,6 +12,12 @@ const NATIVE_READ: &str = include_str!("../../../corpus/m6/native-math-01/read/p...@@ -12,6 +12,12 @@ const NATIVE_READ: &str = include_str!("../../../corpus/m6/native-math-01/read/p
12/// subscripts, a sub-superscript, parentheses, an integral and a sum with limits.12/// subscripts, a sub-superscript, parentheses, an integral and a sum with limits.
13const EDITOR: &[u8] = include_bytes!("../../../corpus/math-edit/native-editor/notebook/links.one");13const EDITOR: &[u8] = include_bytes!("../../../corpus/math-edit/native-editor/notebook/links.one");
14const EDITOR_READ: &str = include_str!("../../../corpus/math-edit/native-editor/read/page-000.xml");14const EDITOR_READ: &str = include_str!("../../../corpus/math-edit/native-editor/read/page-000.xml");
15/// A second editor session: square and n-th roots, a plain fraction, a limit, a product,
16/// square brackets, an overbar and a hat.
17const EDITOR_2: &[u8] =
18 include_bytes!("../../../corpus/math-edit/native-editor-2/notebook/links.one");
19const EDITOR_2_READ: &str =
20 include_str!("../../../corpus/math-edit/native-editor-2/read/page-000.xml");
1521
16fn native_mathml(read: &str) -> Vec<String> {22fn native_mathml(read: &str) -> Vec<String> {
17 read.split("<mml:math")23 read.split("<mml:math")
...@@ -91,6 +97,10 @@ fn equation_editor_expressions_render_to_the_mathml_onenote_exports() {...@@ -91,6 +97,10 @@ fn equation_editor_expressions_render_to_the_mathml_onenote_exports() {
91 rendered_equations(EDITOR, "Read about Rust the Rust site"),97 rendered_equations(EDITOR, "Read about Rust the Rust site"),
92 native_mathml(EDITOR_READ)98 native_mathml(EDITOR_READ)
93 );99 );
100 assert_eq!(
101 rendered_equations(EDITOR_2, "Read about Rust the Rust site"),
102 native_mathml(EDITOR_2_READ)
103 );
94}104}
95105
96fn object(kind: u32, symbols: &str, arguments: Vec<Vec<Math>>) -> Math {106fn object(kind: u32, symbols: &str, arguments: Vec<Vec<Math>>) -> Math {
tools/native_handwriting.py created+67
...@@ -0,0 +1,67 @@
1"""Draw ink over an outline's text and in empty space with the mouse in OneNote 2010 on a
2Rust-written page, then capture the COM read: the oracle for handwriting held by paragraphs.
3
4Usage: native_handwriting.py NOTEBOOK_DIR OUTPUT_DIR
5"""
6import base64
7import json
8import sys
9from pathlib import Path
10import xml.etree.ElementTree as ET
11
12sys.path.insert(0, str(Path(__file__).resolve().parent))
13import native_runner as runner
14
15windows = runner.windows
16
17
18def interaction(name, output):
19 page = ET.parse(sorted((output / 'read').glob('page-*.xml'))[0]).getroot().attrib['ID']
20 script = '''ComObject("OneNote.Application").NavigateTo("PAGE_ID", "", false)
21WinWait("ahk_exe ONENOTE.EXE", , 30)
22WinActivate("ahk_exe ONENOTE.EXE")
23WinWaitActive("ahk_exe ONENOTE.EXE", , 30)
24WinMaximize("ahk_exe ONENOTE.EXE")
25Sleep(3000)
26SendMode("Event")
27CoordMode("Mouse", "Screen")
28Click(258,36)
29Sleep(600)
30Click(205,69)
31Sleep(600)
32; handwriting inside the outline, below its text line
33MouseMove(140,160)
34Click("Down")
35MouseMove(160,175,25)
36MouseMove(180,160,25)
37MouseMove(200,175,25)
38MouseMove(220,160,25)
39Click("Up")
40Sleep(300)
41MouseClickDrag("Left",240,158,300,178,25)
42Sleep(300)
43; a drawing in empty page space
44MouseMove(450,320)
45Click("Down")
46MouseMove(500,360,25)
47MouseMove(450,400,25)
48MouseMove(400,360,25)
49MouseMove(450,320,25)
50Click("Up")
51Sleep(1000)
52Click(21,74)
53Sleep(2000)
54'''.replace('PAGE_ID', page)
55 (output / 'handwriting.ahk').write_text(script)
56 result = windows.do_exec(script, target=name, timeout_ms=120000, shot_delay_ms=1500)
57 screenshot = result.pop('png_b64', None)
58 if screenshot:
59 (output / 'handwriting.png').write_bytes(base64.b64decode(screenshot))
60 (output / 'handwriting.json').write_text(json.dumps(result, indent=2))
61 if result.get('error') or result.get('exit') != 0:
62 raise RuntimeError(str(result))
63
64
65if __name__ == '__main__':
66 notebook, output = Path(sys.argv[1]), Path(sys.argv[2])
67 runner.capture(notebook, output, expected_pages=1, collect_notebook=True, interaction=interaction)
tools/native_math.py+3
...@@ -5,6 +5,7 @@ Usage: native_math.py NOTEBOOK_DIR OUTPUT_DIR...@@ -5,6 +5,7 @@ Usage: native_math.py NOTEBOOK_DIR OUTPUT_DIR
5"""5"""
6import base646import base64
7import json7import json
8import os
8import sys9import sys
9from pathlib import Path10from pathlib import Path
10import xml.etree.ElementTree as ET11import xml.etree.ElementTree as ET
...@@ -23,6 +24,8 @@ EQUATIONS = [...@@ -23,6 +24,8 @@ EQUATIONS = [
23 '\\int_0^1 x dx',24 '\\int_0^1 x dx',
24 '\\sum_(i=1)^n i',25 '\\sum_(i=1)^n i',
25]26]
27if os.environ.get('ONESTORE_MATH_EQUATIONS'):
28 EQUATIONS = os.environ['ONESTORE_MATH_EQUATIONS'].split('|')
2629
2730
28def interaction(name, output):31def interaction(name, output):
tools/test_math_edit.py+1
...@@ -22,6 +22,7 @@ def exported_mathml(path):...@@ -22,6 +22,7 @@ def exported_mathml(path):
22class MathEditTest(unittest.TestCase):22class MathEditTest(unittest.TestCase):
23 def test_equation_editor_expressions_export_as_mathml(self):23 def test_equation_editor_expressions_export_as_mathml(self):
24 self.assertEqual(len(exported_mathml(FIXTURE / 'native-editor/read/page-000.xml')), 5)24 self.assertEqual(len(exported_mathml(FIXTURE / 'native-editor/read/page-000.xml')), 5)
25 self.assertEqual(len(exported_mathml(FIXTURE / 'native-editor-2/read/page-000.xml')), 9)
2526
26 def test_rust_written_equations_export_the_expected_mathml(self):27 def test_rust_written_equations_export_the_expected_mathml(self):
27 with TemporaryDirectory() as temporary:28 with TemporaryDirectory() as temporary: