diff --git a/corpus/math-edit/README.md b/corpus/math-edit/README.md index 304e67a5ab432fa9f62be01d2a078b684b5f3b95..76b3c8eee75a54973364e4a431c410aa5fdc837a 100644 --- a/corpus/math-edit/README.md +++ b/corpus/math-edit/README.md @@ -25,6 +25,16 @@ degree second), a lower limit (19: `munder`, with the upright `lim` as one overbar (23: `mover accent="false"` with a stretchy bar) and an accent (10: `mover accent="true"` with the combining circumflex exported as `^`). +`native-editor-3/` is a third session: `\matrix(1&2@3&4)` (exported inline, +its trailing `(\matrix(a&b@c&d))` staying literal text), `\eqarray(x&=1@y&=2)`, +`x\above 2`, `x\below 2`, `\box(x)`, `\rect(x)`, `\underline(x)` (a literal +`▱` before fences), `\iint x dx dy` (a bare operator), `f(x)/(x^2+1)` and +`\sum^n x`. Its MathML adds matrices (20: `mtable` of cells row by row, the +column count on the opening run's `0x0c003451`), equation arrays (15: rows +with `maligngroup` and a `malignmark` for each `&`), an upper limit (33: +`mover`), boxes (11: `mpadded`; 12: `menclose notation="box"`) and an n-ary +operator with one empty limit (`mover`, or `msup` for integrals). + `written/candidate` is the page writer's output for `equations_are_written_and_read_back`: on a fresh section, every native equation of both fixtures copied as a new paragraph, then two expressions diff --git a/corpus/math-edit/native-editor-3/before-read/environment.json b/corpus/math-edit/native-editor-3/before-read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..17bdc24f0944680caf415698e8c1fa9dd89308c9 --- /dev/null +++ b/corpus/math-edit/native-editor-3/before-read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-22B0E73B", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/math-edit/native-editor-3/before-read/hierarchy.xml b/corpus/math-edit/native-editor-3/before-read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..1728c88c790d94b8aca20c92d9e3737d6a787245 --- /dev/null +++ b/corpus/math-edit/native-editor-3/before-read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/math-edit/native-editor-3/before-read/page-000.xml b/corpus/math-edit/native-editor-3/before-read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..29d91e34592525489edde90a4fd495470924e546 --- /dev/null +++ b/corpus/math-edit/native-editor-3/before-read/page-000.xml @@ -0,0 +1,3 @@ + +the Rust site]]> diff --git a/corpus/math-edit/native-editor-3/before-read/payloads.json b/corpus/math-edit/native-editor-3/before-read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/math-edit/native-editor-3/before-read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/math-edit/native-editor-3/commands.jsonl b/corpus/math-edit/native-editor-3/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..38513d47fc25a01ccdb338cb2cd4289f115d661e --- /dev/null +++ b/corpus/math-edit/native-editor-3/commands.jsonl @@ -0,0 +1,6 @@ +{"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} +{"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} +{"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} +{"command": "powershell -NoProfile -Command \"Rename-Item C:\\one-tests\\runs\\capture\\read before-read\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"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} +{"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} diff --git a/corpus/math-edit/native-editor-3/equations.ahk b/corpus/math-edit/native-editor-3/equations.ahk new file mode 100644 index 0000000000000000000000000000000000000000..facaa25bb9f46c12b0b0e67707cdddee0f35af63 --- /dev/null +++ b/corpus/math-edit/native-editor-3/equations.ahk @@ -0,0 +1,113 @@ +ComObject("OneNote.Application").NavigateTo("{ACFEEB7F-AA18-03BD-3300-EB94605262CF}{1}{B0}", "", false) +WinWait("ahk_exe ONENOTE.EXE", , 30) +WinActivate("ahk_exe ONENOTE.EXE") +WinWaitActive("ahk_exe ONENOTE.EXE", , 30) +WinMaximize("ahk_exe ONENOTE.EXE") +Sleep(3000) +CoordMode("Mouse", "Screen") +Click(300, 140) +Sleep(500) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(500) +Send("!=") +Sleep(600) +SendText("\matrix(1&2@3&4)") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("(\matrix(a&b@c&d))") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\eqarray(x&=1@y&=2)") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("x\above 2") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("x\below 2") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\box(x)") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\rect(x)") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\underline(x)") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\iint x dx dy") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("f(x)/(x^2+1)") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\sum^n x") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Sleep(2000) diff --git a/corpus/math-edit/native-editor-3/equations.json b/corpus/math-edit/native-editor-3/equations.json new file mode 100644 index 0000000000000000000000000000000000000000..83fb4df3362bb2ac7e9fa657fe6db76614b54e31 --- /dev/null +++ b/corpus/math-edit/native-editor-3/equations.json @@ -0,0 +1,13 @@ +{ + "exit": 0, + "stdout": "", + "stderr": "", + "w": 800, + "h": 600, + "error": null, + "win": { + "title": "Read about Rust the Rust site - Microsoft OneNote", + "class": "Framework::CFrame", + "dialog": false + } +} \ No newline at end of file diff --git a/corpus/math-edit/native-editor-3/equations.png b/corpus/math-edit/native-editor-3/equations.png new file mode 100644 index 0000000000000000000000000000000000000000..500c3242eca41b831707f25fc362f3ccc01a50b1 Binary files /dev/null and b/corpus/math-edit/native-editor-3/equations.png differ diff --git a/corpus/math-edit/native-editor-3/machine.json b/corpus/math-edit/native-editor-3/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..d7d4d34d19b6c1181649801adcaf1184d756a7e5 --- /dev/null +++ b/corpus/math-edit/native-editor-3/machine.json @@ -0,0 +1 @@ +{"name": "m6-22b0e73b", "hostname": "ONE-M6-22B0E73B"} diff --git a/corpus/math-edit/native-editor-3/notebook/Open Notebook.onetoc2 b/corpus/math-edit/native-editor-3/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..683f16023dd5b88365e2b90e14aed2142289722a Binary files /dev/null and b/corpus/math-edit/native-editor-3/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/math-edit/native-editor-3/notebook/links.one b/corpus/math-edit/native-editor-3/notebook/links.one new file mode 100644 index 0000000000000000000000000000000000000000..84ad1c8a997f60a3ffb15dff1fdf9a05cf8ebb93 Binary files /dev/null and b/corpus/math-edit/native-editor-3/notebook/links.one differ diff --git a/corpus/math-edit/native-editor-3/read/environment.json b/corpus/math-edit/native-editor-3/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..8e3eb4ad62be1899f637b947aad655f17332a2ae --- /dev/null +++ b/corpus/math-edit/native-editor-3/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-22B0E73B", + "cold": false, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/math-edit/native-editor-3/read/hierarchy.xml b/corpus/math-edit/native-editor-3/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..35fb7bbc0bc299a1ca060e49fe87912606e4a523 --- /dev/null +++ b/corpus/math-edit/native-editor-3/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/math-edit/native-editor-3/read/page-000.xml b/corpus/math-edit/native-editor-3/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..812bc80878b6da3405d3d981abedc93d794a3dff --- /dev/null +++ b/corpus/math-edit/native-editor-3/read/page-000.xml @@ -0,0 +1,4 @@ + +the Rust site]]>1234(\matrix(a&b@c&d)) ]]>x=1y=2]]>x2]]>x2]]>x]]>x]]>▱x]]>∬x dx dy ]]>fxx2+1]]>∑nx ]]> diff --git a/corpus/math-edit/native-editor-3/read/payloads.json b/corpus/math-edit/native-editor-3/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/math-edit/native-editor-3/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/math-edit/native-editor-3/run.json b/corpus/math-edit/native-editor-3/run.json new file mode 100644 index 0000000000000000000000000000000000000000..9e670c7143179c57e2107d7c15c06d0cb8b81a64 --- /dev/null +++ b/corpus/math-edit/native-editor-3/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/Users/clo/dev/one/corpus/link-edit/candidate", + "expected_pages": 1, + "author": null, + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/math-edit/native-editor-3/scripts/cold.ps1 b/corpus/math-edit/native-editor-3/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/math-edit/native-editor-3/scripts/cold.ps1 @@ -0,0 +1,27 @@ +param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } +$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' +if ($CloneHost) { + if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { + throw 'The disposable clone hostname does not match this machine.' + } + New-Item "$key\Options\Paths" -Force | Out-Null + New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null +} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or + -not (Test-Path 'C:\one-tests\profile-original-cache')) { + throw 'Park the personal OneNote profile before resetting the test cache.' +} +$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' +$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) +if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } +New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null +if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } +if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } +New-Item "$key\OpenNotebooks" | Out-Null +New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null diff --git a/corpus/math-edit/native-editor-3/scripts/read.ps1 b/corpus/math-edit/native-editor-3/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/math-edit/native-editor-3/scripts/read.ps1 @@ -0,0 +1,142 @@ +param( + [Parameter(Mandatory=$true)][string]$Root, + [int]$ExpectedPages = -1, + [switch]$UseCurrentCache, + [switch]$Pdf, + [switch]$KeepOpen, + [string]$CloneHost = '' +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +$notebook = Join-Path $root 'notebook' +$output = Join-Path $root 'read' +if (Test-Path $output) { throw 'Choose a new read destination.' } +if ($UseCurrentCache) { + if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { + throw 'Park the personal OneNote profile before reading test notebooks.' + } +} else { + & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost +} +New-Item -ItemType Directory -Path $output | Out-Null +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$failure = $null +try { + $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) + $process = Get-Process ONENOTE + @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; + powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | + ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 + $sections = @() + foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { + $id = '' + $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) + $sections += $id + } + $deadline = [DateTime]::UtcNow.AddSeconds(300) + $previous = '' + $lastChange = '' + $stableSince = [DateTime]::UtcNow + $settled = $false + do { + $pages = @{} + foreach ($section in $sections) { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$xml = $hierarchy + foreach ($node in $xml.SelectNodes('//*[@path]')) { + if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { + throw 'OneNote opened a section outside the copied notebook.' + } + } + foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { + $id = $node.GetAttribute('ID') + $content = '' + $app.GetPageContent($id, [ref]$content, 1, 1) + $pages[$id] = $content + } + } + $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) + if ($signature -ne $previous) { + $lastChange = $previous + $previous = $signature + $stableSince = [DateTime]::UtcNow + } + if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or + ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and + ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { + [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) + $index = 0 + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + $index++ + } + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) + throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." + } + $index = 0 + $payloads = @() + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + if ($Pdf) { + $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) + $app.NavigateTo($id, '', $false) + $app.Publish($id, $pdfPath, 3, '') + if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } + } + [xml]$page = $pages[$id] + foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { + $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) + $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() + [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) + $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); + kind = $file.LocalName; name = $file.GetAttribute('preferredName'); + sha256 = $hash; bytes = $bytes.Length } + } + $index++ + } + [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) + $all = '' + $app.GetHierarchy($notebookId, 4, [ref]$all, 1) + [xml]$finalTree = $all + $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) + if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { + throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' + } + [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) + Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." +} catch { + $failure = $_ + [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + throw +} finally { + try { + try { + if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } + if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { + $app.CloseNotebook($notebookId, $false) + } + } catch { + if ($null -eq $failure) { throw } + [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + } + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + } + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + if (-not $UseCurrentCache -and -not $CloneHost) { + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 + } +} diff --git a/corpus/math-edit/native-editor-3/source.json b/corpus/math-edit/native-editor-3/source.json new file mode 100644 index 0000000000000000000000000000000000000000..14786084a474f1cc0a6f522bd49c8104ad3627f0 --- /dev/null +++ b/corpus/math-edit/native-editor-3/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1604, + "sha256": "6731c4214437fe1cb6aa347b0004542b7f7e4260f47773bf9ec07c4e894de47c", + "mtime_ns": 1789206821144296891 + }, + { + "path": "links.one", + "bytes": 6128, + "sha256": "0cfb356ed374fe919f73fb73fdbe5d66e0baa484d4119f70074d9f869d6db78a", + "mtime_ns": 1789206821116200533 + } +] diff --git a/corpus/math-edit/native-editor-3/teardown.json b/corpus/math-edit/native-editor-3/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/math-edit/native-editor-3/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/crates/onestore/src/document.rs b/crates/onestore/src/document.rs index 1edbd7d627ce6c6d63bf2045d2659e32b0d27b54..d0590f7da2d8b145c496f728a84329d031f7c6f4 100644 --- a/crates/onestore/src/document.rs +++ b/crates/onestore/src/document.rs @@ -136,6 +136,8 @@ format_fields! { pub struct MathObject { pub kind: u32, pub arguments: Option, + /// Column count of a matrix or equation array. + pub columns: Option, pub symbols: Vec, } @@ -232,6 +234,7 @@ impl Revision<'_> { }) }) .transpose()?, + columns: scalar(0x0c003451).and_then(|b| b.first().copied()), symbols: [0x10003453, 0x10003454, 0x10003455] .into_iter() .filter_map(scalar) diff --git a/crates/onestore/src/page/math.rs b/crates/onestore/src/page/math.rs index a822a0b7d7fb75eb776766f9c99c83fa28b2103c..fe596d1829df6f894bd675fab62ecab097f7437c 100644 --- a/crates/onestore/src/page/math.rs +++ b/crates/onestore/src/page/math.rs @@ -1,9 +1,9 @@ //! Equations as OneNote stores them: a linear text where U+FDD0 opens an inline object, //! U+FDEE separates its arguments and U+FDEF closes it, with the object's kind on the run //! data of the opening character. `Math::parse` builds the tree and `mathml` renders it the -//! way OneNote's own export does for the kinds native fixtures have verified (superscript, -//! subscript, both, fraction, brackets, n-ary operators with limits); radicals, under/over -//! limits and the rest follow the text object model's meaning without a native check. +//! way OneNote's own export does, verified against native fixtures for every kind the +//! equation editor produced: scripts, fractions, fences, n-ary operators with any limits, +//! radicals, limit objects, accents, overbars, boxes, matrices and equation arrays. use super::text::Paragraph; use crate::document::MathObject; @@ -18,6 +18,9 @@ pub enum Math { Object { kind: u32, symbols: Vec, + /// Column count of a matrix (arguments are its cells, row by row) or an equation + /// array (arguments are its rows). + columns: Option, arguments: Vec>, }, } @@ -48,6 +51,7 @@ impl Math { struct Frame { kind: u32, symbols: Vec, + columns: Option, arguments: Vec>, } let mut frames: Vec = Vec::new(); @@ -83,6 +87,7 @@ impl Math { frames.push(Frame { kind: object.kind, symbols: object.symbols.clone(), + columns: object.columns, arguments: Vec::new(), }); sequences.push(Vec::new()); @@ -105,6 +110,7 @@ impl Math { sequences.last_mut().unwrap().push(Math::Object { kind: frame.kind, symbols: frame.symbols, + columns: frame.columns, arguments: frame.arguments, }); } @@ -147,6 +153,7 @@ impl Math { Math::Object { kind, symbols, + columns, arguments, } => { // A lone letter, number or operator stands bare; anything else is a row. @@ -176,6 +183,44 @@ impl Math { out.push_str(&format!("")); }; match (kind, arguments.len()) { + (11, 1) => wrapped(out, "mpadded", &[0]), + (12, 1) => { + out.push_str(""); + argument(out, 0); + out.push_str(""); + } + // Matrix cells row by row; an equation array's rows carry `&` alignment + // marks, exported as an alignment group at the row start and a mark at each. + (20, _) | (15, _) => { + let width = if *kind == 20 { + usize::from(columns.unwrap_or(1).max(1)) + } else { + 1 + }; + out.push_str(""); + for (r, row) in arguments.chunks(width).enumerate() { + out.push_str(""); + for (c, cell) in row.iter().enumerate() { + out.push_str(""); + if *kind == 15 { + out.push_str(""); + for node in cell { + match node { + Math::Operator('&') => { + out.push_str("") + } + node => node.write(out), + } + } + } else { + argument(out, r * width + c); + } + out.push_str(""); + } + out.push_str(""); + } + out.push_str(""); + } (31, 2) => wrapped(out, "msup", &[0, 1]), (29, 2) => wrapped(out, "msub", &[0, 1]), (30, 3) => wrapped(out, "msubsup", &[0, 1, 2]), @@ -220,19 +265,29 @@ impl Math { out.push_str(""); } // Lower limit, upper limit, body; integrals take their limits as scripts, - // other operators above and below. + // other operators above and below, and an empty limit leaves its side out. (21, 3) => { let operator = symbols.first().copied().unwrap_or('∑'); - let element = if ('\u{222b}'..='\u{2233}').contains(&operator) { - "msubsup" - } else { - "munderover" + let scripts = ('\u{222b}'..='\u{2233}').contains(&operator); + let (lower, upper) = (!arguments[0].is_empty(), !arguments[1].is_empty()); + let element = match (lower, upper, scripts) { + (true, true, true) => "msubsup", + (true, true, false) => "munderover", + (true, false, true) => "msub", + (true, false, false) => "munder", + (false, true, true) => "msup", + (false, true, false) => "mover", + (false, false, _) => "mrow", }; out.push_str(&format!( "{operator}" )); - argument(out, 0); - argument(out, 1); + if lower { + argument(out, 0); + } + if upper { + argument(out, 1); + } out.push_str(&format!("")); out.push_str(""); for node in &arguments[2] { @@ -320,6 +375,7 @@ impl Math { format.math_object = Some(object.unwrap_or(MathObject { kind: PLAIN_RUN, arguments: None, + columns: None, symbols: Vec::new(), })); (text, format) @@ -341,6 +397,7 @@ fn write_sequence(nodes: &[Math], runs: &mut Vec<(String, Option)>) Math::Object { kind, symbols, + columns, arguments, } => { if !leaf.is_empty() { @@ -351,6 +408,7 @@ fn write_sequence(nodes: &[Math], runs: &mut Vec<(String, Option)>) Some(MathObject { kind: *kind, arguments: Some(arguments.len() as u32), + columns: *columns, symbols: symbols.clone(), }), )); @@ -365,6 +423,7 @@ fn write_sequence(nodes: &[Math], runs: &mut Vec<(String, Option)>) let object = MathObject { kind: *kind, arguments: (index > 0).then_some(index as u32), + columns: None, symbols: Vec::new(), }; let extended = runs.len() > before; diff --git a/crates/onestore/src/page/write.rs b/crates/onestore/src/page/write.rs index 8c2285ac616c3957dbf7d5a44981a2e8c51f70b2..8cb3217bf672b5da8e43c65ec37a6a45bb12d9b5 100644 --- a/crates/onestore/src/page/write.rs +++ b/crates/onestore/src/page/write.rs @@ -1770,6 +1770,9 @@ impl Lowering<'_> { if let Some(count) = object.arguments { set.push((0x14003450, count.to_le_bytes().to_vec())); } + if let Some(columns) = object.columns { + set.push((0x0c003451, vec![columns])); + } for (id, symbol) in [0x10003453, 0x10003454, 0x10003455] .into_iter() .zip(&object.symbols) diff --git a/crates/onestore/tests/page_math.rs b/crates/onestore/tests/page_math.rs index 45e4b4c35ce028c7eb5a84f1abb3229c04fb1ba6..be3249abf1cbe63c07a12e33ec5229c14dd8ebc0 100644 --- a/crates/onestore/tests/page_math.rs +++ b/crates/onestore/tests/page_math.rs @@ -18,6 +18,12 @@ const EDITOR_2: &[u8] = include_bytes!("../../../corpus/math-edit/native-editor-2/notebook/links.one"); const EDITOR_2_READ: &str = include_str!("../../../corpus/math-edit/native-editor-2/read/page-000.xml"); +/// A third session: a matrix, an equation array, upper and lower limit objects, boxes, a +/// nested fraction and a sum with only an upper limit. +const EDITOR_3: &[u8] = + include_bytes!("../../../corpus/math-edit/native-editor-3/notebook/links.one"); +const EDITOR_3_READ: &str = + include_str!("../../../corpus/math-edit/native-editor-3/read/page-000.xml"); fn native_mathml(read: &str) -> Vec { read.split(">) -> Math { Math::Object { kind, symbols: symbols.chars().collect(), + columns: None, arguments, } } diff --git a/tools/test_math_edit.py b/tools/test_math_edit.py index 83227267fa7834792960f48dc92b32a35d597c97..63bd9370a4884f79ceb5adf5cdd1a38516c5fac2 100644 --- a/tools/test_math_edit.py +++ b/tools/test_math_edit.py @@ -23,6 +23,7 @@ class MathEditTest(unittest.TestCase): def test_equation_editor_expressions_export_as_mathml(self): self.assertEqual(len(exported_mathml(FIXTURE / 'native-editor/read/page-000.xml')), 5) self.assertEqual(len(exported_mathml(FIXTURE / 'native-editor-2/read/page-000.xml')), 9) + self.assertEqual(len(exported_mathml(FIXTURE / 'native-editor-3/read/page-000.xml')), 10) def test_rust_written_equations_export_the_expected_mathml(self): with TemporaryDirectory() as temporary: