diff --git a/corpus/ink-edit/README.md b/corpus/ink-edit/README.md index 6c197490dcf039a5f94109e14922dd906bd360c8..3019ee1db0ab1a6f071cff4d43513f04c05f9f20 100644 --- a/corpus/ink-edit/README.md +++ b/corpus/ink-edit/README.md @@ -24,3 +24,9 @@ outline grown to contain the strokes' absolute page coordinates. directories while running the tests, then cold-open each with `tools/native_runner.py OUTPUT COLD --expected-pages 1 --collect-notebook --screenshots`. + +`native-mouse-over-text/` is OneNote 2010 receiving mouse strokes over an +outline's text and in empty space (`tools/native_handwriting.py`): both +became page-level `InkDrawing` objects. Mouse ink never produces +handwriting paragraphs or embedded ink in text runs, so those forms stay +read-only retained content until a pen-authored fixture exists. diff --git a/corpus/ink-edit/native-mouse-over-text/before-read/environment.json b/corpus/ink-edit/native-mouse-over-text/before-read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..48170eb10e7c124bec03bbefa92489448abc1c7b --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/before-read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-9C262230", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/ink-edit/native-mouse-over-text/before-read/hierarchy.xml b/corpus/ink-edit/native-mouse-over-text/before-read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..aafe6fe16b418054fefe67554bd25b4c3a8c2696 --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/before-read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/ink-edit/native-mouse-over-text/before-read/page-000.xml b/corpus/ink-edit/native-mouse-over-text/before-read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..64309b8beab4fba6f61ff424a57c1ff003946095 --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/before-read/page-000.xml @@ -0,0 +1,3 @@ + +the Rust site]]> diff --git a/corpus/ink-edit/native-mouse-over-text/before-read/payloads.json b/corpus/ink-edit/native-mouse-over-text/before-read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/before-read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/ink-edit/native-mouse-over-text/commands.jsonl b/corpus/ink-edit/native-mouse-over-text/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..5396301c658933b761dcd5c744950040598a1a2b --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/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-9C262230 -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-9C262230 -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/ink-edit/native-mouse-over-text/handwriting.ahk b/corpus/ink-edit/native-mouse-over-text/handwriting.ahk new file mode 100644 index 0000000000000000000000000000000000000000..d9eed5cc7ae66da901ae822d1d4a93a9ad4c1be2 --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/handwriting.ahk @@ -0,0 +1,34 @@ +ComObject("OneNote.Application").NavigateTo("{B2A882BE-DBA1-0E8F-3F13-C2840E6C5065}{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) +SendMode("Event") +CoordMode("Mouse", "Screen") +Click(258,36) +Sleep(600) +Click(205,69) +Sleep(600) +; handwriting inside the outline, below its text line +MouseMove(140,160) +Click("Down") +MouseMove(160,175,25) +MouseMove(180,160,25) +MouseMove(200,175,25) +MouseMove(220,160,25) +Click("Up") +Sleep(300) +MouseClickDrag("Left",240,158,300,178,25) +Sleep(300) +; a drawing in empty page space +MouseMove(450,320) +Click("Down") +MouseMove(500,360,25) +MouseMove(450,400,25) +MouseMove(400,360,25) +MouseMove(450,320,25) +Click("Up") +Sleep(1000) +Click(21,74) +Sleep(2000) diff --git a/corpus/ink-edit/native-mouse-over-text/handwriting.json b/corpus/ink-edit/native-mouse-over-text/handwriting.json new file mode 100644 index 0000000000000000000000000000000000000000..83fb4df3362bb2ac7e9fa657fe6db76614b54e31 --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/handwriting.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/ink-edit/native-mouse-over-text/handwriting.png b/corpus/ink-edit/native-mouse-over-text/handwriting.png new file mode 100644 index 0000000000000000000000000000000000000000..70de49a8fabc7dc476321f80881a96835b90d3f1 Binary files /dev/null and b/corpus/ink-edit/native-mouse-over-text/handwriting.png differ diff --git a/corpus/ink-edit/native-mouse-over-text/machine.json b/corpus/ink-edit/native-mouse-over-text/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..f24fec724c6f926a619cc2619251ac87e48c5c43 --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/machine.json @@ -0,0 +1 @@ +{"name": "m6-9c262230", "hostname": "ONE-M6-9C262230"} diff --git a/corpus/ink-edit/native-mouse-over-text/notebook/Open Notebook.onetoc2 b/corpus/ink-edit/native-mouse-over-text/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..e778e73c6e795c17476d16d2c979a1ef85eedad6 Binary files /dev/null and b/corpus/ink-edit/native-mouse-over-text/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/ink-edit/native-mouse-over-text/notebook/links.one b/corpus/ink-edit/native-mouse-over-text/notebook/links.one new file mode 100644 index 0000000000000000000000000000000000000000..a4e70cdeec681787cfac62b97f9d68edc05c8dbb Binary files /dev/null and b/corpus/ink-edit/native-mouse-over-text/notebook/links.one differ diff --git a/corpus/ink-edit/native-mouse-over-text/read/environment.json b/corpus/ink-edit/native-mouse-over-text/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..8a0d4507eddf90ca3c6204a2171411402e8660cc --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-9C262230", + "cold": false, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/ink-edit/native-mouse-over-text/read/hierarchy.xml b/corpus/ink-edit/native-mouse-over-text/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..77f6810a1a91f6557d089eb1644600c7f98c00ce --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/ink-edit/native-mouse-over-text/read/page-000.xml b/corpus/ink-edit/native-mouse-over-text/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..0697afb220185b97a11ad2e68741d3cd0e73de1c --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/read/page-000.xml @@ -0,0 +1,22 @@ + +the Rust site]]>ANoCHQJ6KgFgKgAaH4tCcEWEdYC+0EXZBPqHlTLy7BdBuMAWv/Mr9HreuxOL5V6zSJyboSVat1ES +v+xxzMwisUGJDrsTLO56oVjPVIrml8VPjwb4utLhmyLEZm2nDbJYSZodTkA0BmwQAwtIFETtuZAB +RSNGIwUIC2VPZmdoZGkZFDIIAMAMAgAAyEEzCACwCQIAAMhBZAMVRgBACr4BSIfwsXeFcUmUzTWa +EzmU1mk1gEBmkzgMAgKAQGATWZQCAzOaQGZzSAzOaTWZTOZTNNJrMpmmk1mUzTSazKZppNUymc0m +qZTOZTOH7f7t/5pAZnNIDM5pAZnNJrMpnMpmmk1mUzTSaplMyaTVMpmTSaplMyaTVMpmmk1AAAAF +FEYAQAAACQQFFEYAAAAAEgARILeLLBPpguVJo6ME9ct7COwKP0AjYQMhZDLQO6ADFUYAQAo/QCKS +wgP7+zf4wA==AOMFHQTKAaIBAWAqABofi0JwRYR1gL7QRdkE+oeVMvLsF0G4wBa/8yv0et67E4vlXrNInJuhJVq3 +URK/7HHMzCKxQYkOuxMs7nqhWM9UiuaXxU+PBvi60uGbIsRmbacNslhJmh1OQDQGbBADC0gURO25 +kAFFI0YjBQgLZU9mZ2hkaRkUMggAwAwCAADIQTMIALAJAgAAyEFkAxVGAEAKxQSNAofyDLeQS2Zz +KAwCZzKazSZzCYwGAQGAQGAQGAQGAQFM5pAZnNIDM5pNZlM5pNZlM5pNUymcymaaTWZTNNJqmUzm +k1TKZzSaplM7SIBMJnNJrMpnNEBgEBgEBgEBgEBgEBmkzTKawCZTWaTOZTWaTOZTWaJnMprNEzmU +zmSazRM5lNZomcyms0FpIBAZlM5pNZlMZhAIDAIDAIDAIDAIDAEymsAmU1gEyms0mcyms0mcyms0 +TOZTOZJrNJnMk1miZzKazRM5lNZoLRM5kgMxmU1mkzmU1QCAwCAwCAwCAwCAwCazJM5pAZnNJrMp +nNJrMpnNJqmUzmk1TKZzKZppNUymc0mqZTOaTVMpmIfw++eH0maTWaTOAwCZTWAQGAQFAIDM5pAY +BNZlAJrMoBNZlM5lM5pNUymc0mqZTMmk1TKZppNSZTNNJqTKZplM00gM0mcymsAgM0msAgMAgMAg +MAmsygEBmc0gMzmUAmsymc0msymaaTWZTNNJqTKZppNUymZMpmmk1JlM00mqZYdTSazSAwCazKAQ +GAIDAIDNJnAYBMprAJlNYBMprNJnMpnMk1mkzmSazQmcyTWaJnMiazRM5kTWaJnMi0ppNZlAIDM5 +ogMAgMAgMAmU1gEBmkzTKawCZTWaTOZTWaJnMprNEzmRNZomcyTWaEzmSZzIms0TOZAFFEYAgAAA +CQQFFEYAAAAAEgARIHXkv4XbUV9Btx3j0SO9N8YKABEgsI0hZS1D3QEDFUYAQAoAESDcOupAw5Sd +QA== diff --git a/corpus/ink-edit/native-mouse-over-text/read/payloads.json b/corpus/ink-edit/native-mouse-over-text/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/ink-edit/native-mouse-over-text/run.json b/corpus/ink-edit/native-mouse-over-text/run.json new file mode 100644 index 0000000000000000000000000000000000000000..9e670c7143179c57e2107d7c15c06d0cb8b81a64 --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/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/ink-edit/native-mouse-over-text/scripts/cold.ps1 b/corpus/ink-edit/native-mouse-over-text/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/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/ink-edit/native-mouse-over-text/scripts/read.ps1 b/corpus/ink-edit/native-mouse-over-text/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/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/ink-edit/native-mouse-over-text/source.json b/corpus/ink-edit/native-mouse-over-text/source.json new file mode 100644 index 0000000000000000000000000000000000000000..14786084a474f1cc0a6f522bd49c8104ad3627f0 --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/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/ink-edit/native-mouse-over-text/teardown.json b/corpus/ink-edit/native-mouse-over-text/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/ink-edit/native-mouse-over-text/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/math-edit/README.md b/corpus/math-edit/README.md index 41c825e97f632849b7de0c268b6da349d45c3a74..304e67a5ab432fa9f62be01d2a078b684b5f3b95 100644 --- a/corpus/math-edit/README.md +++ b/corpus/math-edit/README.md @@ -15,6 +15,16 @@ the stored section and compares it with the exported MathML byte for byte; together with `corpus/m6/native-math-01` (superscripts, a fraction, inline Greek) that covers the kinds `onestore::page::Math` claims. +`native-editor-2/` is a second session (`ONESTORE_MATH_EQUATIONS` on +`tools/native_math.py`): `\sqrt x+1`, `\cbrt(x)`, `\sqrt(n&x)`, `a/b`, +`\lim_(x\to 0) f(x)` (the `\lim` stayed literal), `\prod_(k=1)^n k`, +`[a+b]`, `\overline(x)` and `x\hat`; `\sqrt (x+1)` again left an empty +paragraph. Its MathML adds radicals (25: `msqrt`, or `mroot` with the +degree second), a lower limit (19: `munder`, with the upright `lim` as one +`mi` in a row), a product, named fences (`mfenced open="[" close="]"`), an +overbar (23: `mover accent="false"` with a stretchy bar) and an accent (10: +`mover accent="true"` with the combining circumflex exported as `^`). + `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-2/before-read/environment.json b/corpus/math-edit/native-editor-2/before-read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..42f84b6fc2dbbf352f2d0577245ce2bb557c15f6 --- /dev/null +++ b/corpus/math-edit/native-editor-2/before-read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-C31DFB94", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/math-edit/native-editor-2/before-read/hierarchy.xml b/corpus/math-edit/native-editor-2/before-read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..a57e93db3ba3980e173f8bfcf23b4a0efb2b28a5 --- /dev/null +++ b/corpus/math-edit/native-editor-2/before-read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/math-edit/native-editor-2/before-read/page-000.xml b/corpus/math-edit/native-editor-2/before-read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..fa67ad142ef8b71a9ae0cdc02d3bf4cc04039036 --- /dev/null +++ b/corpus/math-edit/native-editor-2/before-read/page-000.xml @@ -0,0 +1,3 @@ + +the Rust site]]> diff --git a/corpus/math-edit/native-editor-2/before-read/payloads.json b/corpus/math-edit/native-editor-2/before-read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/math-edit/native-editor-2/before-read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/math-edit/native-editor-2/commands.jsonl b/corpus/math-edit/native-editor-2/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..decd2dea6386a77a6eb8a9d3b054398420a80d2a --- /dev/null +++ b/corpus/math-edit/native-editor-2/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-C31DFB94 -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-C31DFB94 -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-2/equations.ahk b/corpus/math-edit/native-editor-2/equations.ahk new file mode 100644 index 0000000000000000000000000000000000000000..1f09c896c0d801ac0b64e3d9b516d0afdc1f07b7 --- /dev/null +++ b/corpus/math-edit/native-editor-2/equations.ahk @@ -0,0 +1,104 @@ +ComObject("OneNote.Application").NavigateTo("{2846FEB9-1B89-0608-2064-808478C45E8D}{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("\sqrt x+1") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\sqrt (x+1)") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\cbrt(x)") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\sqrt(n&x)") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("a/b") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\lim_(x\to 0) f(x)") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\prod_(k=1)^n k") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("[a+b]") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("\overline(x)") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Send("!=") +Sleep(600) +SendText("x\hat") +Send(" ") +Sleep(600) +Send("{End}") +Sleep(300) +Send("{Enter}") +Sleep(600) +Sleep(2000) diff --git a/corpus/math-edit/native-editor-2/equations.json b/corpus/math-edit/native-editor-2/equations.json new file mode 100644 index 0000000000000000000000000000000000000000..83fb4df3362bb2ac7e9fa657fe6db76614b54e31 --- /dev/null +++ b/corpus/math-edit/native-editor-2/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-2/equations.png b/corpus/math-edit/native-editor-2/equations.png new file mode 100644 index 0000000000000000000000000000000000000000..aa4789453f675e82152192a262158c2cd7deea7e Binary files /dev/null and b/corpus/math-edit/native-editor-2/equations.png differ diff --git a/corpus/math-edit/native-editor-2/machine.json b/corpus/math-edit/native-editor-2/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..1d2b98c5232a3003763b9fd19208af32c90473be --- /dev/null +++ b/corpus/math-edit/native-editor-2/machine.json @@ -0,0 +1 @@ +{"name": "m6-c31dfb94", "hostname": "ONE-M6-C31DFB94"} diff --git a/corpus/math-edit/native-editor-2/notebook/Open Notebook.onetoc2 b/corpus/math-edit/native-editor-2/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..c23568f599b56d2a22787596ff606c9525c44e9a Binary files /dev/null and b/corpus/math-edit/native-editor-2/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/math-edit/native-editor-2/notebook/links.one b/corpus/math-edit/native-editor-2/notebook/links.one new file mode 100644 index 0000000000000000000000000000000000000000..8366ac8d85d2220aa1c88ec5843b433e587c2b22 Binary files /dev/null and b/corpus/math-edit/native-editor-2/notebook/links.one differ diff --git a/corpus/math-edit/native-editor-2/read/environment.json b/corpus/math-edit/native-editor-2/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..d1628c99dec1df6f1adf3df990fd989b57af406b --- /dev/null +++ b/corpus/math-edit/native-editor-2/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-C31DFB94", + "cold": false, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/math-edit/native-editor-2/read/hierarchy.xml b/corpus/math-edit/native-editor-2/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..b3b602a571daf7729689daf483a4cfce6ea3f32e --- /dev/null +++ b/corpus/math-edit/native-editor-2/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/math-edit/native-editor-2/read/page-000.xml b/corpus/math-edit/native-editor-2/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..e5c9ed12b5b6c41679950cd7d67d6af6c9929c6a --- /dev/null +++ b/corpus/math-edit/native-editor-2/read/page-000.xml @@ -0,0 +1,3 @@ + +the Rust site]]>x+1 ]]>x3]]>xn]]>ab]]>\limx→0fx]]>∏k=1nk ]]>a+b]]>x¯]]>x^]]> diff --git a/corpus/math-edit/native-editor-2/read/payloads.json b/corpus/math-edit/native-editor-2/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/math-edit/native-editor-2/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/math-edit/native-editor-2/run.json b/corpus/math-edit/native-editor-2/run.json new file mode 100644 index 0000000000000000000000000000000000000000..9e670c7143179c57e2107d7c15c06d0cb8b81a64 --- /dev/null +++ b/corpus/math-edit/native-editor-2/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-2/scripts/cold.ps1 b/corpus/math-edit/native-editor-2/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/math-edit/native-editor-2/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-2/scripts/read.ps1 b/corpus/math-edit/native-editor-2/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/math-edit/native-editor-2/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-2/source.json b/corpus/math-edit/native-editor-2/source.json new file mode 100644 index 0000000000000000000000000000000000000000..14786084a474f1cc0a6f522bd49c8104ad3627f0 --- /dev/null +++ b/corpus/math-edit/native-editor-2/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-2/teardown.json b/corpus/math-edit/native-editor-2/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/math-edit/native-editor-2/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/crates/onestore/src/page/math.rs b/crates/onestore/src/page/math.rs index 7ae2f63d495c769e352107e0daab7da9f9d5f289..a822a0b7d7fb75eb776766f9c99c83fa28b2103c 100644 --- a/crates/onestore/src/page/math.rs +++ b/crates/onestore/src/page/math.rs @@ -11,6 +11,8 @@ use crate::document::MathObject; #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum Math { Identifier(char), + /// Upright letters, as the editor stores a function name such as `lim`. + Function(String), Number(String), Operator(char), Object { @@ -51,6 +53,7 @@ impl Math { let mut frames: Vec = Vec::new(); let mut sequences: Vec> = vec![Vec::new()]; let mut number = String::new(); + let mut word = String::new(); let mut offset = 0; for character in paragraph.text().chars() { let here = offset; @@ -63,6 +66,14 @@ impl Math { let number = std::mem::take(&mut number); sequences.last_mut().unwrap().push(Math::Number(number)); } + if character.is_ascii_alphabetic() { + word.push(character); + continue; + } + if !word.is_empty() { + let word = std::mem::take(&mut word); + sequences.last_mut().unwrap().push(Math::Function(word)); + } match character { OBJECT_START => { // The span lookup treats a boundary offset as the span ending there, so the @@ -108,6 +119,9 @@ impl Math { if !number.is_empty() { sequences.last_mut().unwrap().push(Math::Number(number)); } + if !word.is_empty() { + sequences.last_mut().unwrap().push(Math::Function(word)); + } if !frames.is_empty() { return Err(invalid("An equation object never ends")); } @@ -127,6 +141,7 @@ impl Math { fn write(&self, out: &mut String) { match self { Math::Identifier(c) => tag(out, "mi", &c.to_string()), + Math::Function(name) => tag(out, "mi", name), Math::Number(n) => tag(out, "mn", n), Math::Operator(c) => tag(out, "mo", &c.to_string()), Math::Object { @@ -134,12 +149,16 @@ impl Math { symbols, arguments, } => { - // A lone identifier, number or operator stands bare; anything else is a row. + // A lone letter, number or operator stands bare; anything else is a row. let argument = |out: &mut String, index: usize| match arguments .get(index) .map(Vec::as_slice) { - Some([single]) if !matches!(single, Math::Object { .. }) => single.write(out), + Some([single]) + if !matches!(single, Math::Object { .. } | Math::Function(_)) => + { + single.write(out) + } Some(many) => { out.push_str(""); for node in many { @@ -165,8 +184,41 @@ impl Math { (25, _) => wrapped(out, "msqrt", &[arguments.len() - 1]), (19, 2) => wrapped(out, "munder", &[0, 1]), (33, 2) => wrapped(out, "mover", &[0, 1]), - // OneNote exports parentheses as default fences, whatever the pair. - (13, _) => wrapped(out, "mfenced", &[0]), + // Parentheses are the default fences; other pairs name themselves. + (13, _) => { + let (open, close) = (symbols.first(), symbols.get(1)); + if open == Some(&'(') && close == Some(&')') { + wrapped(out, "mfenced", &[0]); + } else { + out.push_str("'); + argument(out, 0); + out.push_str(""); + } + } + (10, 1) | (23, 1) => { + let accent = *kind == 10; + out.push_str(&format!("")); + argument(out, 0); + if accent { + tag( + out, + "mo", + &spacing(symbols.first().copied().unwrap_or('^')).to_string(), + ); + } else { + out.push_str(""); + out.push(symbols.first().copied().unwrap_or('¯')); + out.push_str(""); + } + out.push_str(""); + } // Lower limit, upper limit, body; integrals take their limits as scripts, // other operators above and below. (21, 3) => { @@ -204,6 +256,25 @@ impl Math { } } +fn escaped(c: char) -> String { + match c { + '<' => "<".into(), + '&' => "&".into(), + '"' => """.into(), + c => c.to_string(), + } +} + +/// The spacing character OneNote exports for a combining accent. +fn spacing(c: char) -> char { + match c { + '\u{302}' => '^', + '\u{303}' => '~', + '\u{308}' => '¨', + c => c, + } +} + fn tag(out: &mut String, element: &str, content: &str) { out.push_str(&format!("")); for c in content.chars() { @@ -264,6 +335,7 @@ fn write_sequence(nodes: &[Math], runs: &mut Vec<(String, Option)>) for node in nodes { match node { Math::Identifier(c) => leaf.push(italic(*c)), + Math::Function(name) => leaf.push_str(name), Math::Number(n) => leaf.push_str(n), Math::Operator(c) => leaf.push(*c), Math::Object { diff --git a/crates/onestore/tests/page_math.rs b/crates/onestore/tests/page_math.rs index c62f12b9b596b23eb78a70f9848833a44647f738..45e4b4c35ce028c7eb5a84f1abb3229c04fb1ba6 100644 --- a/crates/onestore/tests/page_math.rs +++ b/crates/onestore/tests/page_math.rs @@ -12,6 +12,12 @@ const NATIVE_READ: &str = include_str!("../../../corpus/m6/native-math-01/read/p /// subscripts, a sub-superscript, parentheses, an integral and a sum with limits. const EDITOR: &[u8] = include_bytes!("../../../corpus/math-edit/native-editor/notebook/links.one"); const EDITOR_READ: &str = include_str!("../../../corpus/math-edit/native-editor/read/page-000.xml"); +/// A second editor session: square and n-th roots, a plain fraction, a limit, a product, +/// square brackets, an overbar and a hat. +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"); fn native_mathml(read: &str) -> Vec { read.split(">) -> Math { diff --git a/tools/native_handwriting.py b/tools/native_handwriting.py new file mode 100644 index 0000000000000000000000000000000000000000..ce00fa7a585787d65b6ea6f2dfd232968cc4d624 --- /dev/null +++ b/tools/native_handwriting.py @@ -0,0 +1,67 @@ +"""Draw ink over an outline's text and in empty space with the mouse in OneNote 2010 on a +Rust-written page, then capture the COM read: the oracle for handwriting held by paragraphs. + +Usage: native_handwriting.py NOTEBOOK_DIR OUTPUT_DIR +""" +import base64 +import json +import sys +from pathlib import Path +import xml.etree.ElementTree as ET + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import native_runner as runner + +windows = runner.windows + + +def interaction(name, output): + page = ET.parse(sorted((output / 'read').glob('page-*.xml'))[0]).getroot().attrib['ID'] + script = '''ComObject("OneNote.Application").NavigateTo("PAGE_ID", "", 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) +SendMode("Event") +CoordMode("Mouse", "Screen") +Click(258,36) +Sleep(600) +Click(205,69) +Sleep(600) +; handwriting inside the outline, below its text line +MouseMove(140,160) +Click("Down") +MouseMove(160,175,25) +MouseMove(180,160,25) +MouseMove(200,175,25) +MouseMove(220,160,25) +Click("Up") +Sleep(300) +MouseClickDrag("Left",240,158,300,178,25) +Sleep(300) +; a drawing in empty page space +MouseMove(450,320) +Click("Down") +MouseMove(500,360,25) +MouseMove(450,400,25) +MouseMove(400,360,25) +MouseMove(450,320,25) +Click("Up") +Sleep(1000) +Click(21,74) +Sleep(2000) +'''.replace('PAGE_ID', page) + (output / 'handwriting.ahk').write_text(script) + result = windows.do_exec(script, target=name, timeout_ms=120000, shot_delay_ms=1500) + screenshot = result.pop('png_b64', None) + if screenshot: + (output / 'handwriting.png').write_bytes(base64.b64decode(screenshot)) + (output / 'handwriting.json').write_text(json.dumps(result, indent=2)) + if result.get('error') or result.get('exit') != 0: + raise RuntimeError(str(result)) + + +if __name__ == '__main__': + notebook, output = Path(sys.argv[1]), Path(sys.argv[2]) + runner.capture(notebook, output, expected_pages=1, collect_notebook=True, interaction=interaction) diff --git a/tools/native_math.py b/tools/native_math.py index 749c70f1c3b4076e6a41fe9d14fd4769c72424cb..94f381f5ff329df3a0ae1d0d510c93880ac5fb9a 100644 --- a/tools/native_math.py +++ b/tools/native_math.py @@ -5,6 +5,7 @@ Usage: native_math.py NOTEBOOK_DIR OUTPUT_DIR """ import base64 import json +import os import sys from pathlib import Path import xml.etree.ElementTree as ET @@ -23,6 +24,8 @@ EQUATIONS = [ '\\int_0^1 x dx', '\\sum_(i=1)^n i', ] +if os.environ.get('ONESTORE_MATH_EQUATIONS'): + EQUATIONS = os.environ['ONESTORE_MATH_EQUATIONS'].split('|') def interaction(name, output): diff --git a/tools/test_math_edit.py b/tools/test_math_edit.py index 2d6866d8e785b558777dfb321b72a8d4b4a2ffa4..83227267fa7834792960f48dc92b32a35d597c97 100644 --- a/tools/test_math_edit.py +++ b/tools/test_math_edit.py @@ -22,6 +22,7 @@ def exported_mathml(path): 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) def test_rust_written_equations_export_the_expected_mathml(self): with TemporaryDirectory() as temporary: