| author | |
| committer | |
| log | 59c8648048632812f85fd595c05aa9720ad2a051 |
| tree | 2bad0c7aaf539ac8292d94301a97f32d68d6c791 |
| parent | 8b4f45b5b7a6d9b8555e7234a123c9887495d265 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Table structure in the page model is a difference the writer now lowers.
Native rows and cells are plain containers: a new cell carries the
modification time, the indent array copied from a sibling cell and the flags
every native cell has; a new row lists its cells; every table has its row and
cell order rebuilt to the model, and its column count, widths (in inches),
lock bitmap and border flag rewritten. A new table starts as an empty text
paragraph placed like any other, whose content the structure pass replaces
with the table before the deferred cell containers receive their paragraphs.
Paragraphs of removed rows and cells are covered by the removal. Table
identity, layout and tags, and cell layout, indents and shading stay fixed;
an emptied cell keeps the replacement paragraph the tree writer provides.
corpus/table-edit is the native gate: a Rust-created two-by-two table on a
fresh page, and a native table on the tree fixture with a third column and a
second row added, both cold-read by OneNote 2010 with widths, locks, borders,
rows and cell text intact; tools/test_table_edit.py checks both without a VM.
Assisted-by: claude-fable-5.148 files changed, 1219 insertions(+), 46 deletions(-)
corpus/table-edit/README.md created+28| ... | @@ -0,0 +1,28 @@ | ||
| 1 | # Rust table authoring | ||
| 2 | |||
| 3 | `created/candidate` is the page writer's output for | ||
| 4 | `a_new_table_is_created_on_a_fresh_page` in | ||
| 5 | `crates/onestore/tests/page_tables.rs`: on a section created in Rust, a | ||
| 6 | paragraph between two text paragraphs becomes a two-by-two table with locked | ||
| 7 | 144 pt and 216 pt columns, visible borders and a text paragraph in every | ||
| 8 | cell. `created/cold` is a fresh OneNote 2010 read showing both columns with | ||
| 9 | their widths and locks, both rows and every cell text. | ||
| 10 | |||
| 11 | `edited/candidate` is the writer's output for | ||
| 12 | `a_row_and_a_column_are_added_to_a_native_table_and_removed_again` on | ||
| 13 | `corpus/outline-edit/tree/before`: the one-row, two-column native table on | ||
| 14 | "Delete cell subtree" gains an unlocked 120 pt third column and a second row | ||
| 15 | of three cells. `edited/cold` is its cold read: the native columns keep their | ||
| 16 | widths and locks, OneNote sizes the unlocked column to its content, and the | ||
| 17 | new row and cells read back with their text. | ||
| 18 | |||
| 19 | Native rows and cells are plain containers; the writer creates them with the | ||
| 20 | modification time, the indent array copied from a sibling cell, and the flags | ||
| 21 | every native cell carries, rebuilds each table's row and cell order to the | ||
| 22 | model's, and writes the column count, widths, locks and border flag. New | ||
| 23 | tables start as an empty paragraph whose content the structure pass replaces. | ||
| 24 | An emptied cell keeps a replacement paragraph, as the tree writer provides. | ||
| 25 | `tools/test_table_edit.py` checks both captures without a VM. Regenerate with | ||
| 26 | `ONESTORE_TABLE_EXPORT` and `ONESTORE_TABLE_EDIT_EXPORT` set to new absolute | ||
| 27 | directories while running the tests, then cold-open them with | ||
| 28 | `tools/native_runner.py OUTPUT COLD --expected-pages 1|12 --collect-notebook`. | ||
corpus/table-edit/created/candidate/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/table-edit/created/candidate/Open Notebook.onetoc2 differ | |||
corpus/table-edit/created/candidate/tables.one created| Binary files /dev/null and b/corpus/table-edit/created/candidate/tables.one differ | |||
corpus/table-edit/created/cold/commands.jsonl created+3| ... | @@ -0,0 +1,3 @@ | ||
| 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-838F23A9 -ExpectedPages 1", "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} | ||
corpus/table-edit/created/cold/machine.json created+1| ... | @@ -0,0 +1 @@ | ||
| 1 | {"name": "m6-838f23a9", "hostname": "ONE-M6-838F23A9"} | ||
corpus/table-edit/created/cold/notebook/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/table-edit/created/cold/notebook/Open Notebook.onetoc2 differ | |||
corpus/table-edit/created/cold/notebook/tables.one created| Binary files /dev/null and b/corpus/table-edit/created/cold/notebook/tables.one differ | |||
corpus/table-edit/created/cold/read/environment.json created+7| ... | @@ -0,0 +1,7 @@ | ||
| 1 | { | ||
| 2 | "powershell": "5.1.14409.1005", | ||
| 3 | "schema": "xs2010", | ||
| 4 | "hostname": "ONE-M6-838F23A9", | ||
| 5 | "cold": true, | ||
| 6 | "onenote": "14.0.4763.1000" | ||
| 7 | } | ||
corpus/table-edit/created/cold/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="{1BB56A3F-9C37-4838-B671-213C7548F681}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-12T18:57:57.000Z" color="#B49EDE"><one:Section name="tables" ID="{1C2D662F-7BFD-0276-1BFA-B3E7E7E01562}{1}{B0}" path="C:\one-tests\runs\capture\notebook\tables.one" lastModifiedTime="2026-09-12T18:57:57.000Z" color="#8AA8E4"><one:Page ID="{1A74DE87-A9CF-0A2A-057E-CDD4A592DF04}{1}{B0}" name="Before the table" dateTime="2026-09-12T18:55:42.000Z" lastModifiedTime="2026-09-12T18:57:46.000Z" pageLevel="1"/></one:Section></one:Notebook> | ||
corpus/table-edit/created/cold/read/page-000.xml created+2| ... | @@ -0,0 +1,2 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{1A74DE87-A9CF-0A2A-057E-CDD4A592DF04}{1}{B0}" name="Before the table" dateTime="2026-09-12T18:55:42.000Z" lastModifiedTime="2026-09-12T18:57:46.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-12T18:55:42.000Z" objectID="{1C2D662F-7BFD-0276-1BFA-B3E7E7E01562}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="395.1299438476562" height="67.21086883544922"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T18:55:42.000Z" lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{1C2D662F-7BFD-0276-1BFA-B3E7E7E01562}{24}{B0}" alignment="left"><one:T><![CDATA[Before the table]]></one:T></one:OE><one:OE author="Table author" lastModifiedBy="Table author" creationTime="2026-09-12T18:55:42.000Z" lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{3A30E194-5242-09E5-2837-A8BE31520023}{0}{B0}" alignment="left"><one:Table bordersVisible="true" lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{2BAC850E-BEEF-0DFE-101D-E7A08FA3EF36}{0}{B0}"><one:Columns><one:Column index="0" width="144.0" isLocked="true"/><one:Column index="1" width="216.0" isLocked="true"/></one:Columns><one:Row objectID="{32B84587-8B60-0900-2C7D-8B03AA1021FC}{0}{B0}" lastModifiedTime="2026-09-12T18:55:42.000Z"><one:Cell lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{35FB065B-82B3-06B6-23B5-4A866D422733}{0}{B0}"><one:OEChildren><one:OE creationTime="2026-09-12T18:55:42.000Z" lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{6961CA5C-5601-0109-3ABA-9C6E1D87807B}{0}{B0}" alignment="left"><one:T><![CDATA[Name]]></one:T></one:OE></one:OEChildren></one:Cell><one:Cell lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{6460510E-A7E1-04D3-21FE-7B255A488496}{0}{B0}"><one:OEChildren><one:OE creationTime="2026-09-12T18:55:42.000Z" lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{251F55AF-E7E8-0E35-3FFF-A0188DB1E006}{0}{B0}" alignment="left"><one:T><![CDATA[Value]]></one:T></one:OE></one:OEChildren></one:Cell></one:Row><one:Row objectID="{E5A88DF0-4872-0A5B-03CE-6AD2D88CC6A2}{0}{B0}" lastModifiedTime="2026-09-12T18:55:42.000Z"><one:Cell lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{65FC0F6F-A7F8-01D8-2F86-E81451D010FB}{0}{B0}"><one:OEChildren><one:OE creationTime="2026-09-12T18:55:42.000Z" lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{1F1346CC-AB72-0AC2-0F02-C82B60ACE63B}{0}{B0}" alignment="left"><one:T><![CDATA[Rust &#129408;]]></one:T></one:OE></one:OEChildren></one:Cell><one:Cell lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{4A655FAB-04B2-0641-3895-C87F7F551A21}{0}{B0}"><one:OEChildren><one:OE creationTime="2026-09-12T18:55:42.000Z" lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{5BBFE54E-D6A1-0A3C-0302-2F0ED58341F1}{0}{B0}" alignment="left"><one:T><![CDATA[Written natively]]></one:T></one:OE></one:OEChildren></one:Cell></one:Row></one:Table></one:OE><one:OE author="Table author" lastModifiedBy="Table author" creationTime="2026-09-12T18:55:42.000Z" lastModifiedTime="2026-09-12T18:55:42.000Z" objectID="{A750F1FF-86E2-0E24-2656-CFDC574202E9}{0}{B0}" alignment="left"><one:T><![CDATA[After the table]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/created/cold/read/payloads.json created+3| ... | @@ -0,0 +1,3 @@ | ||
| 1 | [ | ||
| 2 | |||
| 3 | ] | ||
| \ No newline at end of file | |||
corpus/table-edit/created/cold/run.json created+18| ... | @@ -0,0 +1,18 @@ | ||
| 1 | { | ||
| 2 | "notebook": "/private/tmp/m4/tables", | ||
| 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/table-edit/created/cold/scripts/cold.ps1 created+27| ... | @@ -0,0 +1,27 @@ | ||
| 1 | param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') | ||
| 2 | Set-StrictMode -Version Latest | ||
| 3 | $ErrorActionPreference = 'Stop' | ||
| 4 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | ||
| 5 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | ||
| 6 | throw 'Choose a run directly below C:\one-tests\runs.' | ||
| 7 | } | ||
| 8 | if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } | ||
| 9 | $key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' | ||
| 10 | if ($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)) | ||
| 22 | if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } | ||
| 23 | New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null | ||
| 24 | if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } | ||
| 25 | if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } | ||
| 26 | New-Item "$key\OpenNotebooks" | Out-Null | ||
| 27 | New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null | ||
corpus/table-edit/created/cold/scripts/read.ps1 created+142| ... | @@ -0,0 +1,142 @@ | ||
| 1 | param( | ||
| 2 | [Parameter(Mandatory=$true)][string]$Root, | ||
| 3 | [int]$ExpectedPages = -1, | ||
| 4 | [switch]$UseCurrentCache, | ||
| 5 | [switch]$Pdf, | ||
| 6 | [switch]$KeepOpen, | ||
| 7 | [string]$CloneHost = '' | ||
| 8 | ) | ||
| 9 | Set-StrictMode -Version Latest | ||
| 10 | $ErrorActionPreference = 'Stop' | ||
| 11 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | ||
| 12 | if ([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' | ||
| 17 | if (Test-Path $output) { throw 'Choose a new read destination.' } | ||
| 18 | if ($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 | } | ||
| 25 | New-Item -ItemType Directory -Path $output | Out-Null | ||
| 26 | $app = New-Object -ComObject OneNote.Application | ||
| 27 | $notebookId = '' | ||
| 28 | $failure = $null | ||
| 29 | try { | ||
| 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/table-edit/created/cold/source.json created+14| ... | @@ -0,0 +1,14 @@ | ||
| 1 | [ | ||
| 2 | { | ||
| 3 | "path": "Open Notebook.onetoc2", | ||
| 4 | "bytes": 1604, | ||
| 5 | "sha256": "b39b2e142e0adce0de37ad336f40c3e9c2ba92ce6927c9d109139e74c1fb4a6e", | ||
| 6 | "mtime_ns": 1789239342755220465 | ||
| 7 | }, | ||
| 8 | { | ||
| 9 | "path": "tables.one", | ||
| 10 | "bytes": 11016, | ||
| 11 | "sha256": "e1c014220e05352a01c98752031b32c17512407295aa812ff8e9c776e48eef6e", | ||
| 12 | "mtime_ns": 1789239342754903465 | ||
| 13 | } | ||
| 14 | ] | ||
corpus/table-edit/created/cold/teardown.json created+1| ... | @@ -0,0 +1 @@ | ||
| 1 | {"absent": true} | ||
corpus/table-edit/edited/candidate/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/table-edit/edited/candidate/Open Notebook.onetoc2 differ | |||
corpus/table-edit/edited/candidate/synthetic.one created| Binary files /dev/null and b/corpus/table-edit/edited/candidate/synthetic.one differ | |||
corpus/table-edit/edited/cold/commands.jsonl created+3| ... | @@ -0,0 +1,3 @@ | ||
| 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-C8AB6EC4 -ExpectedPages 12", "exit": 0, "stdout": "Read 1 sections and 12 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} | ||
corpus/table-edit/edited/cold/machine.json created+1| ... | @@ -0,0 +1 @@ | ||
| 1 | {"name": "m6-c8ab6ec4", "hostname": "ONE-M6-C8AB6EC4"} | ||
corpus/table-edit/edited/cold/notebook/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/table-edit/edited/cold/notebook/Open Notebook.onetoc2 differ | |||
corpus/table-edit/edited/cold/notebook/synthetic.one created| Binary files /dev/null and b/corpus/table-edit/edited/cold/notebook/synthetic.one differ | |||
corpus/table-edit/edited/cold/read/environment.json created+7| ... | @@ -0,0 +1,7 @@ | ||
| 1 | { | ||
| 2 | "powershell": "5.1.14409.1005", | ||
| 3 | "schema": "xs2010", | ||
| 4 | "hostname": "ONE-M6-C8AB6EC4", | ||
| 5 | "cold": true, | ||
| 6 | "onenote": "14.0.4763.1000" | ||
| 7 | } | ||
corpus/table-edit/edited/cold/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="{9FF765D4-5CAF-4188-BB5E-68A856C47912}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-12T19:13:51.000Z" color="#B49EDE"><one:Section name="synthetic" ID="{71EAF2FB-8947-040C-3465-46DA73F0DC1F}{1}{B0}" path="C:\one-tests\runs\capture\notebook\synthetic.one" lastModifiedTime="2026-09-12T19:13:51.000Z" color="#8AA8E4"><one:Page ID="{5DA3017D-008C-471D-B302-EE4B8FC8C000}{1}{B0}" name="Bold 🦀 italic é color 東京" dateTime="2026-09-05T05:06:54.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/><one:Page ID="{6C7C7CE2-D252-45A4-87CC-219E590F55E7}{1}{B0}" name="Indent first subtree" dateTime="2026-09-08T19:25:53.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/><one:Page ID="{2D84D722-F6DE-4CDD-854C-8D00E3B76567}{1}{B0}" name="Outdent first group" dateTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/><one:Page ID="{1AC02AC7-668D-45E8-B568-2738A2B8A4D7}{1}{B0}" name="Delete only grouped subtree" dateTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/><one:Page ID="{5DB95A90-D897-4722-B2D6-9EE974B6BEFC}{1}{B0}" name="Delete unindented sibling after group" dateTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/><one:Page ID="{FBAED6A0-87A8-48FC-B1FB-DD9E6EFF9255}{1}{B0}" name="Delete cell subtree" dateTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/><one:Page ID="{7C5EF51D-1A31-48BF-8872-5291C89EF4FC}{1}{B0}" name="Delete sole cell paragraph" dateTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/><one:Page ID="{CA2332E9-C7A2-4A38-A470-7DEADE848A47}{1}{B0}" name="Move numbered subtree down" dateTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/><one:Page ID="{4B73B2A4-5353-42D4-89E8-DC422EA67897}{1}{B0}" name="Delete numbered subtree" dateTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/><one:Page ID="{21DE3D4A-0957-4483-A838-66DA5528B09A}{1}{B0}" name="Indent bullet subtree" dateTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/><one:Page ID="{D5D0795C-AD9E-40A4-BF9F-BD3AE2BA774D}{1}{B0}" name="Outdent child across indentation gap" dateTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/><one:Page ID="{0D06F5CA-2987-4CE6-B19F-21DF40642FFE}{1}{B0}" name="Move cell subtree down" dateTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1"/></one:Section></one:Notebook> | ||
corpus/table-edit/edited/cold/read/page-000.xml created+5| ... | @@ -0,0 +1,5 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{0D06F5CA-2987-4CE6-B19F-21DF40642FFE}{1}{B0}" name="Move cell subtree down" dateTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Preserved task"/><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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Move cell subtree down]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{39}{B0}"><one:Position x="72.0" y="108.0" z="0"/><one:Size width="428.1300048828125" height="62.23086929321289"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{40}{B0}" alignment="left"><one:Table bordersVisible="true" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{41}{B0}"><one:Columns><one:Column index="0" width="240.0" isLocked="true"/><one:Column index="1" width="180.0" isLocked="true"/></one:Columns><one:Row objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{42}{B0}" lastModifiedTime="2026-09-08T19:26:01.000Z"><one:Cell lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{43}{B0}" lastModifiedByInitials="V"><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{46}{B0}" alignment="left" quickStyleIndex="0"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:T><![CDATA[<span | ||
| 3 | style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a | ||
| 4 | href="https://example.invalid/tree"><span lang=x-none>Link label</span></a>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{55}{B0}" alignment="left" quickStyleIndex="0"><one:T style="font-family:Calibri;font-size:11.0pt"><![CDATA[<span | ||
| 5 | style='font-style:italic'>Child</span>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{61}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Grandchild]]></one:T></one:OE></one:OEChildren></one:OE></one:OEChildren></one:OE><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{66}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Cell tail]]></one:T></one:OE></one:OEChildren></one:Cell><one:Cell lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{71}{B0}" lastModifiedByInitials="V"><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{74}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other cell]]></one:T></one:OE></one:OEChildren></one:Cell></one:Row></one:Table></one:OE></one:OEChildren></one:Outline><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{79}{B0}"><one:Position x="540.0" y="108.0" z="1"/><one:Size width="99.85796356201171" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{6633A1B8-7648-4E6E-9FBC-293654CB1441}{80}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other outline]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/page-001.xml created+5| ... | @@ -0,0 +1,5 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{1AC02AC7-668D-45E8-B568-2738A2B8A4D7}{1}{B0}" name="Delete only grouped subtree" dateTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Preserved task"/><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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{05DCEE34-B75F-48EC-8381-5E3516F5AC23}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Delete only grouped subtree]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{05DCEE34-B75F-48EC-8381-5E3516F5AC23}{39}{B0}"><one:Position x="72.0" y="108.0" z="0"/><one:Size width="169.6107788085937" height="53.71086883544922"/><one:OEChildren indent="2"><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{05DCEE34-B75F-48EC-8381-5E3516F5AC23}{40}{B0}" alignment="left" quickStyleIndex="0"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:T><![CDATA[<span | ||
| 3 | style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a | ||
| 4 | href="https://example.invalid/tree"><span lang=x-none>Link label</span></a>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{05DCEE34-B75F-48EC-8381-5E3516F5AC23}{49}{B0}" alignment="left" quickStyleIndex="0"><one:T style="font-family:Calibri;font-size:11.0pt"><![CDATA[<span | ||
| 5 | style='font-style:italic'>Child</span>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{05DCEE34-B75F-48EC-8381-5E3516F5AC23}{55}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Grandchild]]></one:T></one:OE></one:OEChildren></one:OE></one:OEChildren></one:OE></one:OEChildren><one:OEChildren><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{05DCEE34-B75F-48EC-8381-5E3516F5AC23}{60}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Trailing sibling]]></one:T></one:OE></one:OEChildren></one:Outline><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{05DCEE34-B75F-48EC-8381-5E3516F5AC23}{66}{B0}"><one:Position x="540.0" y="108.0" z="1"/><one:Size width="99.85796356201171" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{05DCEE34-B75F-48EC-8381-5E3516F5AC23}{67}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other outline]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/page-002.xml created+5| ... | @@ -0,0 +1,5 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{21DE3D4A-0957-4483-A838-66DA5528B09A}{1}{B0}" name="Indent bullet subtree" dateTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Preserved task"/><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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{18F54FB8-EC4C-4CA1-B46B-AA3A4579D397}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Indent bullet subtree]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{18F54FB8-EC4C-4CA1-B46B-AA3A4579D397}{39}{B0}"><one:Position x="72.0" y="108.0" z="0"/><one:Size width="142.6107788085937" height="67.13858032226562"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{18F54FB8-EC4C-4CA1-B46B-AA3A4579D397}{40}{B0}" alignment="left" quickStyleIndex="0"><one:List><one:Bullet bullet="3" fontSize="11.0"/></one:List><one:T><![CDATA[Anchor]]></one:T></one:OE><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{18F54FB8-EC4C-4CA1-B46B-AA3A4579D397}{46}{B0}" alignment="left" quickStyleIndex="0"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:List><one:Bullet bullet="3" fontSize="11.0"/></one:List><one:T><![CDATA[<span | ||
| 3 | style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a | ||
| 4 | href="https://example.invalid/tree"><span lang=x-none>Link label</span></a>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{18F54FB8-EC4C-4CA1-B46B-AA3A4579D397}{55}{B0}" alignment="left" quickStyleIndex="0"><one:T style="font-family:Calibri;font-size:11.0pt"><![CDATA[<span | ||
| 5 | style='font-style:italic'>Child</span>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{18F54FB8-EC4C-4CA1-B46B-AA3A4579D397}{61}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Grandchild]]></one:T></one:OE></one:OEChildren></one:OE></one:OEChildren></one:OE><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{18F54FB8-EC4C-4CA1-B46B-AA3A4579D397}{67}{B0}" alignment="left" quickStyleIndex="0"><one:List><one:Bullet bullet="3" fontSize="11.0"/></one:List><one:T><![CDATA[Trailing sibling]]></one:T></one:OE></one:OEChildren></one:Outline><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{18F54FB8-EC4C-4CA1-B46B-AA3A4579D397}{73}{B0}"><one:Position x="540.0" y="108.0" z="1"/><one:Size width="99.85796356201171" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{18F54FB8-EC4C-4CA1-B46B-AA3A4579D397}{74}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other outline]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/page-003.xml created+5| ... | @@ -0,0 +1,5 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{2D84D722-F6DE-4CDD-854C-8D00E3B76567}{1}{B0}" name="Outdent first group" dateTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Preserved task"/><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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{B6F3BCAF-7E3F-4B35-BE7D-45552CA4B5DD}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Outdent first group]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{B6F3BCAF-7E3F-4B35-BE7D-45552CA4B5DD}{39}{B0}"><one:Position x="72.0" y="108.0" z="0"/><one:Size width="169.6107788085937" height="53.71086883544922"/><one:OEChildren indent="2"><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{B6F3BCAF-7E3F-4B35-BE7D-45552CA4B5DD}{40}{B0}" alignment="left" quickStyleIndex="0"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:T><![CDATA[<span | ||
| 3 | style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a | ||
| 4 | href="https://example.invalid/tree"><span lang=x-none>Link label</span></a>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{B6F3BCAF-7E3F-4B35-BE7D-45552CA4B5DD}{49}{B0}" alignment="left" quickStyleIndex="0"><one:T style="font-family:Calibri;font-size:11.0pt"><![CDATA[<span | ||
| 5 | style='font-style:italic'>Child</span>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{B6F3BCAF-7E3F-4B35-BE7D-45552CA4B5DD}{55}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Grandchild]]></one:T></one:OE></one:OEChildren></one:OE></one:OEChildren></one:OE></one:OEChildren><one:OEChildren><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{B6F3BCAF-7E3F-4B35-BE7D-45552CA4B5DD}{60}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Trailing sibling]]></one:T></one:OE></one:OEChildren></one:Outline><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{B6F3BCAF-7E3F-4B35-BE7D-45552CA4B5DD}{66}{B0}"><one:Position x="540.0" y="108.0" z="1"/><one:Size width="99.85796356201171" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{B6F3BCAF-7E3F-4B35-BE7D-45552CA4B5DD}{67}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other outline]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/page-004.xml created+5| ... | @@ -0,0 +1,5 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{4B73B2A4-5353-42D4-89E8-DC422EA67897}{1}{B0}" name="Delete numbered subtree" dateTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Preserved task"/><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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{75D1FB33-7916-4B9E-A6E8-001B9EBC5D73}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Delete numbered subtree]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{75D1FB33-7916-4B9E-A6E8-001B9EBC5D73}{39}{B0}"><one:Position x="72.0" y="108.0" z="0"/><one:Size width="142.6107788085937" height="67.13858032226562"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{75D1FB33-7916-4B9E-A6E8-001B9EBC5D73}{40}{B0}" alignment="left" quickStyleIndex="0"><one:List><one:Number numberSequence="1" numberFormat="##." fontSize="11.0" font="Calibri" text="I."/></one:List><one:T><![CDATA[Anchor]]></one:T></one:OE><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{75D1FB33-7916-4B9E-A6E8-001B9EBC5D73}{46}{B0}" alignment="left" quickStyleIndex="0"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:List><one:Number numberSequence="1" numberFormat="##." fontSize="11.0" font="Calibri" text="II."/></one:List><one:T><![CDATA[<span | ||
| 3 | style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a | ||
| 4 | href="https://example.invalid/tree"><span lang=x-none>Link label</span></a>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{75D1FB33-7916-4B9E-A6E8-001B9EBC5D73}{55}{B0}" alignment="left" quickStyleIndex="0"><one:T style="font-family:Calibri;font-size:11.0pt"><![CDATA[<span | ||
| 5 | style='font-style:italic'>Child</span>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{75D1FB33-7916-4B9E-A6E8-001B9EBC5D73}{61}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Grandchild]]></one:T></one:OE></one:OEChildren></one:OE></one:OEChildren></one:OE><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{75D1FB33-7916-4B9E-A6E8-001B9EBC5D73}{67}{B0}" alignment="left" quickStyleIndex="0"><one:List><one:Number numberSequence="1" numberFormat="##." fontSize="11.0" font="Calibri" text="III."/></one:List><one:T><![CDATA[Trailing sibling]]></one:T></one:OE></one:OEChildren></one:Outline><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{75D1FB33-7916-4B9E-A6E8-001B9EBC5D73}{73}{B0}"><one:Position x="540.0" y="108.0" z="1"/><one:Size width="99.85796356201171" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{75D1FB33-7916-4B9E-A6E8-001B9EBC5D73}{74}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other outline]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/page-005.xml created+20| ... | @@ -0,0 +1,20 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{5DA3017D-008C-471D-B302-EE4B8FC8C000}{1}{B0}" name="Bold 🦀 italic é color 東京" dateTime="2026-09-05T05:06:54.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" 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="Rich author" lastModifiedBy="Rich author" lastModifiedTime="2026-09-08T04:47:24.000Z" objectID="{3A6FA9C1-4BF9-44AB-BCF0-629070542561}{10}{B0}"><one:Position x="36.0" y="14.40000057220459" z="0"/><one:Size width="127.5136947631836" height="42.90299224853515"/><one:OEChildren><one:OE creationTime="2026-09-08T04:47:24.000Z" lastModifiedTime="2026-09-08T04:47:24.000Z" objectID="{3D0A0136-B884-0FA1-0CA4-A19ABB3F5797}{1}{B0}" alignment="left"><one:T><![CDATA[<span style='font-weight:bold; | ||
| 3 | text-decoration:underline;font-family:Calibri;font-size:11.0pt'>Bold</span><span | ||
| 4 | style='font-family:Calibri;font-size:11.0pt'> </span><span style='font-family: | ||
| 5 | Arial;font-size:13.5pt'>&#129408;</span><span style='font-family:Calibri; | ||
| 6 | font-size:11.0pt'> </span><span style='font-style:italic;text-decoration:line-through; | ||
| 7 | font-family:Calibri;font-size:11.0pt'>italic</span><span style='font-family: | ||
| 8 | Calibri;font-size:11.0pt'> </span><span style='vertical-align:super;font-family: | ||
| 9 | Calibri;font-size:11.0pt'>é</span><span style='font-family:Calibri;font-size: | ||
| 10 | 11.0pt'> </span><span style='font-family:Calibri;font-size:11.0pt;color:#0C2238; | ||
| 11 | background:yellow;mso-highlight:yellow'>color</span><span style='font-family: | ||
| 12 | Calibri;font-size:11.0pt'> </span><span style='vertical-align:sub;font-family: | ||
| 13 | Calibri;font-size:11.0pt'>東京</span><span style='font-family:Calibri;font-size: | ||
| 14 | 11.0pt'><br> | ||
| 15 | End</span>]]></one:T></one:OE><one:OE author="snow" authorInitials="S" lastModifiedBy="snow" lastModifiedByInitials="S" creationTime="2026-09-05T05:06:55.000Z" lastModifiedTime="2026-09-05T05:06:56.000Z" objectID="{3A6FA9C1-4BF9-44AB-BCF0-629070542561}{11}{B0}" alignment="left" quickStyleIndex="0" style="font-size:11.0pt;color:#1F4E79"><one:T><![CDATA[<span | ||
| 16 | style='font-weight:bold;font-family:Calibri' lang=en-US>Fictitious: café, </span><span | ||
| 17 | style='font-weight:bold;font-family:SimSun' lang=en-US>東京</span><span | ||
| 18 | style='font-weight:bold;font-family:Calibri' lang=en-US>, </span><span | ||
| 19 | style='font-weight:bold;font-family:Arial;direction:rtl;unicode-bidi:embed' | ||
| 20 | lang=ar-SA>مرحبا</span>]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/page-006.xml created+4| ... | @@ -0,0 +1,4 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{5DB95A90-D897-4722-B2D6-9EE974B6BEFC}{1}{B0}" name="Delete unindented sibling after group" dateTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Preserved task"/><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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-08T19:25:56.000Z" objectID="{2CEBFD60-F6B9-4214-9C7A-617F29495BA3}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Delete unindented sibling after group]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{2CEBFD60-F6B9-4214-9C7A-617F29495BA3}{39}{B0}"><one:Position x="72.0" y="108.0" z="0"/><one:Size width="129.6621246337891" height="26.85543441772461"/><one:OEChildren indent="2"><one:OE creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-08T19:25:56.000Z" objectID="{2CEBFD60-F6B9-4214-9C7A-617F29495BA3}{40}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Anchor]]></one:T></one:OE></one:OEChildren><one:OEChildren><one:OE creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-08T19:25:56.000Z" objectID="{2CEBFD60-F6B9-4214-9C7A-617F29495BA3}{45}{B0}" alignment="left" quickStyleIndex="0"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:T><![CDATA[<span | ||
| 3 | style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a | ||
| 4 | href="https://example.invalid/tree"><span lang=x-none>Link label</span></a>]]></one:T></one:OE></one:OEChildren></one:Outline><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{2CEBFD60-F6B9-4214-9C7A-617F29495BA3}{55}{B0}"><one:Position x="540.0" y="108.0" z="1"/><one:Size width="99.85796356201171" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-08T19:25:56.000Z" objectID="{2CEBFD60-F6B9-4214-9C7A-617F29495BA3}{56}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other outline]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/page-007.xml created+5| ... | @@ -0,0 +1,5 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{6C7C7CE2-D252-45A4-87CC-219E590F55E7}{1}{B0}" name="Indent first subtree" dateTime="2026-09-08T19:25:53.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Preserved task"/><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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{A329CBF5-153D-448A-A5E1-6412E2DB8701}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Indent first subtree]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{A329CBF5-153D-448A-A5E1-6412E2DB8701}{39}{B0}"><one:Position x="72.0" y="108.0" z="0"/><one:Size width="142.6107788085937" height="53.71086883544922"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:54.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{A329CBF5-153D-448A-A5E1-6412E2DB8701}{40}{B0}" alignment="left" quickStyleIndex="0"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:T><![CDATA[<span | ||
| 3 | style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a | ||
| 4 | href="https://example.invalid/tree"><span lang=x-none>Link label</span></a>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{A329CBF5-153D-448A-A5E1-6412E2DB8701}{49}{B0}" alignment="left" quickStyleIndex="0"><one:T style="font-family:Calibri;font-size:11.0pt"><![CDATA[<span | ||
| 5 | style='font-style:italic'>Child</span>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{A329CBF5-153D-448A-A5E1-6412E2DB8701}{55}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Grandchild]]></one:T></one:OE></one:OEChildren></one:OE></one:OEChildren></one:OE><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{A329CBF5-153D-448A-A5E1-6412E2DB8701}{60}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Trailing sibling]]></one:T></one:OE></one:OEChildren></one:Outline><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{A329CBF5-153D-448A-A5E1-6412E2DB8701}{65}{B0}"><one:Position x="540.0" y="108.0" z="1"/><one:Size width="99.85796356201171" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:55.000Z" lastModifiedTime="2026-09-08T19:25:55.000Z" objectID="{A329CBF5-153D-448A-A5E1-6412E2DB8701}{66}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other outline]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/page-008.xml created+4| ... | @@ -0,0 +1,4 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{7C5EF51D-1A31-48BF-8872-5291C89EF4FC}{1}{B0}" name="Delete sole cell paragraph" dateTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Preserved task"/><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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{9720D3D0-E051-4491-AC18-194FA8D7FCA9}{17}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Delete sole cell paragraph]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{9720D3D0-E051-4491-AC18-194FA8D7FCA9}{23}{B0}"><one:Position x="72.0" y="108.0" z="0"/><one:Size width="428.1300048828125" height="21.94771385192871"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{9720D3D0-E051-4491-AC18-194FA8D7FCA9}{24}{B0}" alignment="left"><one:Table bordersVisible="true" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{9720D3D0-E051-4491-AC18-194FA8D7FCA9}{25}{B0}"><one:Columns><one:Column index="0" width="240.0" isLocked="true"/><one:Column index="1" width="180.0" isLocked="true"/></one:Columns><one:Row objectID="{9720D3D0-E051-4491-AC18-194FA8D7FCA9}{26}{B0}" lastModifiedTime="2026-09-08T19:26:01.000Z"><one:Cell lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{9720D3D0-E051-4491-AC18-194FA8D7FCA9}{27}{B0}" lastModifiedByInitials="V"><one:OEChildren><one:OE creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{9720D3D0-E051-4491-AC18-194FA8D7FCA9}{30}{B0}" alignment="left" quickStyleIndex="0"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:T><![CDATA[<span | ||
| 3 | style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a | ||
| 4 | href="https://example.invalid/tree"><span lang=x-none>Link label</span></a>]]></one:T></one:OE></one:OEChildren></one:Cell><one:Cell lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{9720D3D0-E051-4491-AC18-194FA8D7FCA9}{39}{B0}" lastModifiedByInitials="V"><one:OEChildren><one:OE creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{9720D3D0-E051-4491-AC18-194FA8D7FCA9}{42}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other cell]]></one:T></one:OE></one:OEChildren></one:Cell></one:Row></one:Table></one:OE></one:OEChildren></one:Outline><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{9720D3D0-E051-4491-AC18-194FA8D7FCA9}{47}{B0}"><one:Position x="540.0" y="108.0" z="1"/><one:Size width="99.85796356201171" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{9720D3D0-E051-4491-AC18-194FA8D7FCA9}{48}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other outline]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/page-009.xml created+5| ... | @@ -0,0 +1,5 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{CA2332E9-C7A2-4A38-A470-7DEADE848A47}{1}{B0}" name="Move numbered subtree down" dateTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Preserved task"/><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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{A018D6FA-C6E3-4E5B-811F-BC2668588F96}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Move numbered subtree down]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{A018D6FA-C6E3-4E5B-811F-BC2668588F96}{39}{B0}"><one:Position x="72.0" y="108.0" z="0"/><one:Size width="142.6107788085937" height="67.13858032226562"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{A018D6FA-C6E3-4E5B-811F-BC2668588F96}{40}{B0}" alignment="left" quickStyleIndex="0"><one:List><one:Number numberSequence="1" numberFormat="##." fontSize="11.0" font="Calibri" text="I."/></one:List><one:T><![CDATA[Anchor]]></one:T></one:OE><one:OE creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{A018D6FA-C6E3-4E5B-811F-BC2668588F96}{46}{B0}" alignment="left" quickStyleIndex="0"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:List><one:Number numberSequence="1" numberFormat="##." fontSize="11.0" font="Calibri" text="II."/></one:List><one:T><![CDATA[<span | ||
| 3 | style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a | ||
| 4 | href="https://example.invalid/tree"><span lang=x-none>Link label</span></a>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{A018D6FA-C6E3-4E5B-811F-BC2668588F96}{55}{B0}" alignment="left" quickStyleIndex="0"><one:T style="font-family:Calibri;font-size:11.0pt"><![CDATA[<span | ||
| 5 | style='font-style:italic'>Child</span>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{A018D6FA-C6E3-4E5B-811F-BC2668588F96}{61}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Grandchild]]></one:T></one:OE></one:OEChildren></one:OE></one:OEChildren></one:OE><one:OE creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{A018D6FA-C6E3-4E5B-811F-BC2668588F96}{67}{B0}" alignment="left" quickStyleIndex="0"><one:List><one:Number numberSequence="1" numberFormat="##." fontSize="11.0" font="Calibri" text="III."/></one:List><one:T><![CDATA[Trailing sibling]]></one:T></one:OE></one:OEChildren></one:Outline><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{A018D6FA-C6E3-4E5B-811F-BC2668588F96}{73}{B0}"><one:Position x="540.0" y="108.0" z="1"/><one:Size width="99.85796356201171" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:57.000Z" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{A018D6FA-C6E3-4E5B-811F-BC2668588F96}{74}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other outline]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/page-010.xml created+5| ... | @@ -0,0 +1,5 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{D5D0795C-AD9E-40A4-BF9F-BD3AE2BA774D}{1}{B0}" name="Outdent child across indentation gap" dateTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Preserved task"/><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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{F0E05943-CCA2-4653-82B2-274C624DB573}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Outdent child across indentation gap]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{F0E05943-CCA2-4653-82B2-274C624DB573}{39}{B0}"><one:Position x="72.0" y="108.0" z="0"/><one:Size width="196.6107788085937" height="80.56629943847656"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{F0E05943-CCA2-4653-82B2-274C624DB573}{40}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Anchor]]></one:T><one:OEChildren indent="2"><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{F0E05943-CCA2-4653-82B2-274C624DB573}{45}{B0}" alignment="left" quickStyleIndex="0"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:T><![CDATA[<span | ||
| 3 | style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a | ||
| 4 | href="https://example.invalid/tree"><span lang=x-none>Link label</span></a>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{F0E05943-CCA2-4653-82B2-274C624DB573}{54}{B0}" alignment="left" quickStyleIndex="0"><one:T style="font-family:Calibri;font-size:11.0pt"><![CDATA[<span | ||
| 5 | style='font-style:italic'>Child</span>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{F0E05943-CCA2-4653-82B2-274C624DB573}{60}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Grandchild]]></one:T></one:OE></one:OEChildren></one:OE></one:OEChildren></one:OE><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{F0E05943-CCA2-4653-82B2-274C624DB573}{65}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other child]]></one:T></one:OE></one:OEChildren></one:OE><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{F0E05943-CCA2-4653-82B2-274C624DB573}{70}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Trailing sibling]]></one:T></one:OE></one:OEChildren></one:Outline><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:26:01.000Z" objectID="{F0E05943-CCA2-4653-82B2-274C624DB573}{76}{B0}"><one:Position x="540.0" y="108.0" z="1"/><one:Size width="99.85796356201171" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:58.000Z" lastModifiedTime="2026-09-08T19:25:58.000Z" objectID="{F0E05943-CCA2-4653-82B2-274C624DB573}{77}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other outline]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/page-011.xml created+5| ... | @@ -0,0 +1,5 @@ | ||
| 1 | <?xml version="1.0"?> | ||
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{FBAED6A0-87A8-48FC-B1FB-DD9E6EFF9255}{1}{B0}" name="Delete cell subtree" dateTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-12T19:13:42.000Z" pageLevel="1" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Preserved task"/><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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-08T19:25:56.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Delete cell subtree]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-12T18:55:43.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{39}{B0}"><one:Position x="72.0" y="108.0" z="0"/><one:Size width="470.2199096679687" height="120.9217376708984"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-12T18:55:43.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{40}{B0}" alignment="left"><one:Table bordersVisible="true" lastModifiedTime="2026-09-12T18:55:43.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{41}{B0}"><one:Columns><one:Column index="0" width="240.0" isLocked="true"/><one:Column index="1" width="180.0" isLocked="true"/><one:Column index="2" width="37.11000061035156"/></one:Columns><one:Row objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{42}{B0}" lastModifiedTime="2026-09-12T18:55:43.000Z"><one:Cell lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{43}{B0}" lastModifiedByInitials="V"><one:OEChildren><one:OE creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-08T19:25:56.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{46}{B0}" alignment="left" quickStyleIndex="0"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:T><![CDATA[<span | ||
| 3 | style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a | ||
| 4 | href="https://example.invalid/tree"><span lang=x-none>Link label</span></a>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-08T19:25:56.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{55}{B0}" alignment="left" quickStyleIndex="0"><one:T style="font-family:Calibri;font-size:11.0pt"><![CDATA[<span | ||
| 5 | style='font-style:italic'>Child</span>]]></one:T><one:OEChildren><one:OE creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-08T19:25:56.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{61}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Grandchild]]></one:T></one:OE></one:OEChildren></one:OE></one:OEChildren></one:OE><one:OE creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-08T19:25:56.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{66}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Cell tail]]></one:T></one:OE></one:OEChildren></one:Cell><one:Cell lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{71}{B0}" lastModifiedByInitials="V"><one:OEChildren><one:OE creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-08T19:25:56.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{74}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other cell]]></one:T></one:OE></one:OEChildren></one:Cell><one:Cell lastModifiedTime="2026-09-12T18:55:43.000Z" objectID="{C14CF8A0-032E-0395-11DE-B62C5DCE853B}{0}{B0}" style="font-family:Calibri;font-size:11.0pt" author="Table author" lastModifiedBy="Table author"><one:OEChildren><one:OE creationTime="2026-09-12T18:55:43.000Z" lastModifiedTime="2026-09-12T18:55:43.000Z" objectID="{407559B6-BB85-0193-0722-C32CEE2C3710}{0}{B0}" alignment="left"><one:T><![CDATA[Third column]]></one:T></one:OE></one:OEChildren></one:Cell></one:Row><one:Row objectID="{28136D1F-A94C-0160-29C1-0D2EABDD0BCD}{0}{B0}" lastModifiedTime="2026-09-12T18:55:43.000Z" style="font-family:Calibri;font-size:11.0pt" author="Table author" lastModifiedBy="Table author"><one:Cell lastModifiedTime="2026-09-12T18:55:43.000Z" objectID="{F0C0FF05-D8DF-03FA-1FEA-D7EE6C20B308}{0}{B0}"><one:OEChildren><one:OE creationTime="2026-09-12T18:55:43.000Z" lastModifiedTime="2026-09-12T18:55:43.000Z" objectID="{4FCEE4C9-410B-0257-06A6-9B7676785603}{0}{B0}" alignment="left"><one:T><![CDATA[Second row]]></one:T></one:OE></one:OEChildren></one:Cell><one:Cell lastModifiedTime="2026-09-12T18:55:43.000Z" objectID="{E40BDB03-7325-0C0F-0CBC-34F0A3CB81BE}{0}{B0}"><one:OEChildren><one:OE creationTime="2026-09-12T18:55:43.000Z" lastModifiedTime="2026-09-12T18:55:43.000Z" objectID="{53323A03-7C00-01CA-00B3-77649749EB4A}{0}{B0}" alignment="left"><one:T><![CDATA[Second row, second column]]></one:T></one:OE></one:OEChildren></one:Cell><one:Cell lastModifiedTime="2026-09-12T18:55:43.000Z" objectID="{F375BD16-25B5-0C8D-358D-B1173B4ABFE1}{0}{B0}"><one:OEChildren><one:OE creationTime="2026-09-12T18:55:43.000Z" lastModifiedTime="2026-09-12T18:55:43.000Z" objectID="{02FF20F1-303E-0742-100D-DF7F0C257477}{0}{B0}" alignment="left"><one:T><![CDATA[Second row, third column]]></one:T></one:OE></one:OEChildren></one:Cell></one:Row></one:Table></one:OE></one:OEChildren></one:Outline><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-08T19:25:57.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{79}{B0}"><one:Position x="540.0" y="108.0" z="1"/><one:Size width="99.85796356201171" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-08T19:25:56.000Z" lastModifiedTime="2026-09-08T19:25:56.000Z" objectID="{659E1897-02A3-4667-BF86-EB87B6C71310}{80}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Other outline]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/table-edit/edited/cold/read/payloads.json created+3| ... | @@ -0,0 +1,3 @@ | ||
| 1 | [ | ||
| 2 | |||
| 3 | ] | ||
| \ No newline at end of file | |||
corpus/table-edit/edited/cold/run.json created+18| ... | @@ -0,0 +1,18 @@ | ||
| 1 | { | ||
| 2 | "notebook": "/private/tmp/m4/table-edit", | ||
| 3 | "expected_pages": 12, | ||
| 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/table-edit/edited/cold/scripts/cold.ps1 created+27| ... | @@ -0,0 +1,27 @@ | ||
| 1 | param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') | ||
| 2 | Set-StrictMode -Version Latest | ||
| 3 | $ErrorActionPreference = 'Stop' | ||
| 4 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | ||
| 5 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | ||
| 6 | throw 'Choose a run directly below C:\one-tests\runs.' | ||
| 7 | } | ||
| 8 | if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } | ||
| 9 | $key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' | ||
| 10 | if ($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)) | ||
| 22 | if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } | ||
| 23 | New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null | ||
| 24 | if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } | ||
| 25 | if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } | ||
| 26 | New-Item "$key\OpenNotebooks" | Out-Null | ||
| 27 | New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null | ||
corpus/table-edit/edited/cold/scripts/read.ps1 created+142| ... | @@ -0,0 +1,142 @@ | ||
| 1 | param( | ||
| 2 | [Parameter(Mandatory=$true)][string]$Root, | ||
| 3 | [int]$ExpectedPages = -1, | ||
| 4 | [switch]$UseCurrentCache, | ||
| 5 | [switch]$Pdf, | ||
| 6 | [switch]$KeepOpen, | ||
| 7 | [string]$CloneHost = '' | ||
| 8 | ) | ||
| 9 | Set-StrictMode -Version Latest | ||
| 10 | $ErrorActionPreference = 'Stop' | ||
| 11 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | ||
| 12 | if ([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' | ||
| 17 | if (Test-Path $output) { throw 'Choose a new read destination.' } | ||
| 18 | if ($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 | } | ||
| 25 | New-Item -ItemType Directory -Path $output | Out-Null | ||
| 26 | $app = New-Object -ComObject OneNote.Application | ||
| 27 | $notebookId = '' | ||
| 28 | $failure = $null | ||
| 29 | try { | ||
| 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/table-edit/edited/cold/source.json created+14| ... | @@ -0,0 +1,14 @@ | ||
| 1 | [ | ||
| 2 | { | ||
| 3 | "path": "Open Notebook.onetoc2", | ||
| 4 | "bytes": 1612, | ||
| 5 | "sha256": "f49128a77aa74d62e6a1b1281abf6fd76f0789bf90cf47d812162404995c4276", | ||
| 6 | "mtime_ns": 1789239343363554023 | ||
| 7 | }, | ||
| 8 | { | ||
| 9 | "path": "synthetic.one", | ||
| 10 | "bytes": 92288, | ||
| 11 | "sha256": "1d0632852ecbf66bff16e9b36e2dfa3df059bdfd15807f4943bfeabda0e920ce", | ||
| 12 | "mtime_ns": 1789239343362662188 | ||
| 13 | } | ||
| 14 | ] | ||
corpus/table-edit/edited/cold/teardown.json created+1| ... | @@ -0,0 +1 @@ | ||
| 1 | {"absent": true} | ||
crates/notebook/README.md+1-1| ... | @@ -14,7 +14,7 @@ diffs the supplied model against the page stored in the supplied local snapshot, | ... | @@ -14,7 +14,7 @@ diffs the supplied model against the page stored in the supplied local snapshot, |
| 14 | writes the difference into the working image and queues one | 14 | writes the difference into the working image and queues one |
| 15 | `Operation::Page(PageIntent { before, after, author })`; `before` is the page the | 15 | `Operation::Page(PageIntent { before, after, author })`; `before` is the page the |
| 16 | edit started from and is the precondition reconciliation checks. Text, styles, | 16 | edit started from and is the precondition reconciliation checks. Text, styles, |
| 17 | paragraph formatting, hyperlinks, bullets, numbering and note tags, paragraph structure, | 17 | paragraph formatting, hyperlinks, bullets, numbering, note tags, table rows and columns, paragraph structure, |
| 18 | outline layout, insertions and deletions are all differences between `before` and | 18 | outline layout, insertions and deletions are all differences between `before` and |
| 19 | `after`; the library never sees editor operations. | 19 | `after`; the library never sees editor operations. |
| 20 | 20 |
crates/onestore/README.md+1-1| ... | @@ -57,7 +57,7 @@ harness also accepts `--client-profile release`. | ... | @@ -57,7 +57,7 @@ harness also accepts `--client-profile release`. |
| 57 | | `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules | | 57 | | `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules | |
| 58 | | `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | | 58 | | `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | |
| 59 | | `OutlineEdit`, `PreparedEdit::outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content | | 59 | | `OutlineEdit`, `PreparedEdit::outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content | |
| 60 | | `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks, bullets and numbering, note tags, paragraph insertion/split/join/move/deletion, outline insertion/deletion/position/width and saved collapse state, lowered onto the typed writers with the model's identities | | 60 | | `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks, bullets and numbering, note tags, table rows and columns, paragraph insertion/split/join/move/deletion, outline insertion/deletion/position/width and saved collapse state, lowered onto the typed writers with the model's identities | |
| 61 | | `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically | | 61 | | `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically | |
| 62 | | `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter | | 62 | | `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter | |
| 63 | | `read_file` | Read a snapshot under whole-file exclusion | | 63 | | `read_file` | Read a snapshot under whole-file exclusion | |
crates/onestore/src/page/write.rs+313-43| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | //! Publishes an edited page model by lowering the difference from the stored page onto | 1 | //! Publishes an edited page model by lowering the difference from the stored page onto |
| 2 | //! the typed writers, then squashing their transactions into one revision per space. | 2 | //! the typed writers, then squashing their transactions into one revision per space. |
| 3 | 3 | ||
| 4 | use super::{Outline, Page, PageObject, PageParagraph, ParagraphContent}; | 4 | use super::{Outline, Page, PageObject, PageParagraph, ParagraphContent, Table}; |
| 5 | use crate::{ | 5 | use crate::{ |
| 6 | Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets, | 6 | Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets, |
| 7 | RevisionIndex, Store, TextAttribute, TreeEdit, Value, | 7 | RevisionIndex, Store, TextAttribute, TreeEdit, Value, |
| ... | @@ -20,6 +20,50 @@ fn invalid(message: &'static str) -> Error { | ... | @@ -20,6 +20,50 @@ fn invalid(message: &'static str) -> Error { |
| 20 | Error { offset: 0, message } | 20 | Error { offset: 0, message } |
| 21 | } | 21 | } |
| 22 | 22 | ||
| 23 | /// A native measurement array: a count byte, `header - 1` reserved bytes, then each value | ||
| 24 | /// in inches. | ||
| 25 | fn measurement_bytes(values: &[f32], header: usize) -> Result<Vec<u8>, Error> { | ||
| 26 | let count = u8::try_from(values.len()) | ||
| 27 | .map_err(|_| invalid("A measurement array exceeds the document range"))?; | ||
| 28 | let mut bytes = vec![0u8; header]; | ||
| 29 | bytes[0] = count; | ||
| 30 | for value in values { | ||
| 31 | if !value.is_finite() { | ||
| 32 | return Err(invalid("A measurement must be finite")); | ||
| 33 | } | ||
| 34 | bytes.extend_from_slice(&(value / 36.0).to_le_bytes()); | ||
| 35 | } | ||
| 36 | Ok(bytes) | ||
| 37 | } | ||
| 38 | |||
| 39 | /// Every row carries one cell per column and widths are usable. | ||
| 40 | fn validate_table(table: &Table) -> Result<(), Error> { | ||
| 41 | if table.rows.is_empty() || table.columns.is_empty() { | ||
| 42 | return Err(invalid("A table needs at least one row and one column")); | ||
| 43 | } | ||
| 44 | if table | ||
| 45 | .columns | ||
| 46 | .iter() | ||
| 47 | .any(|c| !c.width.is_finite() || c.width < 36.0) | ||
| 48 | { | ||
| 49 | return Err(invalid("Table columns are at least 36 points wide")); | ||
| 50 | } | ||
| 51 | for row in &table.rows { | ||
| 52 | if row.cells.len() != table.columns.len() { | ||
| 53 | return Err(invalid("Every table row has one cell per column")); | ||
| 54 | } | ||
| 55 | for cell in &row.cells { | ||
| 56 | // An emptied cell keeps a replacement paragraph, as the tree writer provides. | ||
| 57 | for paragraph in &cell.paragraphs { | ||
| 58 | if let ParagraphContent::Table(nested) = &paragraph.content { | ||
| 59 | validate_table(nested)?; | ||
| 60 | } | ||
| 61 | } | ||
| 62 | } | ||
| 63 | } | ||
| 64 | Ok(()) | ||
| 65 | } | ||
| 66 | |||
| 23 | pub(crate) fn write_page( | 67 | pub(crate) fn write_page( |
| 24 | source: &[u8], | 68 | source: &[u8], |
| 25 | space: ExGuid, | 69 | space: ExGuid, |
| ... | @@ -154,6 +198,16 @@ impl Lowering<'_> { | ... | @@ -154,6 +198,16 @@ impl Lowering<'_> { |
| 154 | self.alias.get(&model).copied().unwrap_or(model) | 198 | self.alias.get(&model).copied().unwrap_or(model) |
| 155 | } | 199 | } |
| 156 | 200 | ||
| 201 | /// A writer identity for a new object; squash renames it to the model's. | ||
| 202 | fn allocate(&mut self, model: ExGuid) -> Result<ExGuid, Error> { | ||
| 203 | let written = ExGuid { | ||
| 204 | guid: crate::write::fresh_guid()?, | ||
| 205 | n: 1, | ||
| 206 | }; | ||
| 207 | self.alias.insert(model, written); | ||
| 208 | Ok(written) | ||
| 209 | } | ||
| 210 | |||
| 157 | fn apply(&mut self, edit: impl FnOnce(&[u8]) -> Result<Vec<u8>, Error>) -> Result<(), Error> { | 211 | fn apply(&mut self, edit: impl FnOnce(&[u8]) -> Result<Vec<u8>, Error>) -> Result<(), Error> { |
| 158 | self.image = edit(&self.image)?; | 212 | self.image = edit(&self.image)?; |
| 159 | Ok(()) | 213 | Ok(()) |
| ... | @@ -187,6 +241,11 @@ impl Lowering<'_> { | ... | @@ -187,6 +241,11 @@ impl Lowering<'_> { |
| 187 | } | 241 | } |
| 188 | let old = View::new(before)?; | 242 | let old = View::new(before)?; |
| 189 | let new = View::new(after)?; | 243 | let new = View::new(after)?; |
| 244 | for paragraph in new.paragraphs.values() { | ||
| 245 | if let ParagraphContent::Table(table) = &paragraph.content { | ||
| 246 | validate_table(table)?; | ||
| 247 | } | ||
| 248 | } | ||
| 190 | self.check_fixed_objects(before, after, &old, &new)?; | 249 | self.check_fixed_objects(before, after, &old, &new)?; |
| 191 | for id in new.outlines.keys().chain(new.paragraphs.keys()) { | 250 | for id in new.outlines.keys().chain(new.paragraphs.keys()) { |
| 192 | if !old.outlines.contains_key(id) | 251 | if !old.outlines.contains_key(id) |
| ... | @@ -295,31 +354,28 @@ impl Lowering<'_> { | ... | @@ -295,31 +354,28 @@ impl Lowering<'_> { |
| 295 | } | 354 | } |
| 296 | } | 355 | } |
| 297 | (ParagraphContent::Table(table), ParagraphContent::Table(previous)) => { | 356 | (ParagraphContent::Table(table), ParagraphContent::Table(previous)) => { |
| 298 | let same = table.id == previous.id | 357 | if table.id != previous.id |
| 299 | && table.columns == previous.columns | 358 | || table.layout != previous.layout |
| 300 | && table.borders == previous.borders | 359 | || table.tags != previous.tags |
| 301 | && table.layout == previous.layout | 360 | { |
| 302 | && table.tags == previous.tags | 361 | return Err(invalid("Table identity, layout and tags cannot be edited")); |
| 303 | && table.rows.len() == previous.rows.len() | 362 | } |
| 304 | && table | 363 | let unchanged_cells = |
| 305 | .rows | 364 | table.rows.iter().flat_map(|row| &row.cells).all(|cell| { |
| 306 | .iter() | 365 | previous |
| 307 | .zip(&previous.rows) | 366 | .rows |
| 308 | .all(|(row, previous)| { | 367 | .iter() |
| 309 | row.id == previous.id | 368 | .flat_map(|row| &row.cells) |
| 310 | && row.cells.len() == previous.cells.len() | 369 | .find(|before| before.id == cell.id) |
| 311 | && row.cells.iter().zip(&previous.cells).all( | 370 | .is_none_or(|before| { |
| 312 | |(cell, previous)| { | 371 | cell.layout == before.layout |
| 313 | cell.id == previous.id | 372 | && cell.indents == before.indents |
| 314 | && cell.layout == previous.layout | 373 | && cell.shading == before.shading |
| 315 | && cell.indents == previous.indents | 374 | && cell.unsupported == before.unsupported |
| 316 | && cell.shading == previous.shading | 375 | }) |
| 317 | && cell.unsupported == previous.unsupported | 376 | }); |
| 318 | }, | 377 | if !unchanged_cells { |
| 319 | ) | 378 | return Err(invalid("Cell layout, indents and shading cannot be edited")); |
| 320 | }); | ||
| 321 | if !same { | ||
| 322 | return Err(invalid("Table structure cannot be edited")); | ||
| 323 | } | 379 | } |
| 324 | } | 380 | } |
| 325 | (ParagraphContent::Unsupported(a), ParagraphContent::Unsupported(b)) if a == b => {} | 381 | (ParagraphContent::Unsupported(a), ParagraphContent::Unsupported(b)) if a == b => {} |
| ... | @@ -544,10 +600,30 @@ impl Lowering<'_> { | ... | @@ -544,10 +600,30 @@ impl Lowering<'_> { |
| 544 | collect_containers(&outline.paragraphs, &mut containers); | 600 | collect_containers(&outline.paragraphs, &mut containers); |
| 545 | } | 601 | } |
| 546 | } | 602 | } |
| 603 | // Cells that do not exist yet receive their paragraphs once the table structure does. | ||
| 604 | let (existing, deferred): (Vec<ExGuid>, Vec<ExGuid>) = | ||
| 605 | containers.into_iter().partition(|container| { | ||
| 606 | old.children.contains_key(container) | ||
| 607 | || new.paragraphs.contains_key(container) | ||
| 608 | || new.outlines.contains_key(container) | ||
| 609 | }); | ||
| 610 | self.place_containers(old, new, placed, &existing)?; | ||
| 611 | self.edit_table_structure(old, new)?; | ||
| 612 | self.place_containers(old, new, placed, &deferred)?; | ||
| 613 | Ok(()) | ||
| 614 | } | ||
| 615 | |||
| 616 | fn place_containers( | ||
| 617 | &mut self, | ||
| 618 | old: &View<'_>, | ||
| 619 | new: &View<'_>, | ||
| 620 | placed: &BTreeMap<ExGuid, Vec<ExGuid>>, | ||
| 621 | containers: &[ExGuid], | ||
| 622 | ) -> Result<(), Error> { | ||
| 547 | for container in containers { | 623 | for container in containers { |
| 548 | let after = &new.children[&container]; | 624 | let after = &new.children[container]; |
| 549 | let current: Vec<ExGuid> = placed | 625 | let current: Vec<ExGuid> = placed |
| 550 | .get(&container) | 626 | .get(container) |
| 551 | .map(|list| { | 627 | .map(|list| { |
| 552 | list.iter() | 628 | list.iter() |
| 553 | .copied() | 629 | .copied() |
| ... | @@ -561,26 +637,35 @@ impl Lowering<'_> { | ... | @@ -561,26 +637,35 @@ impl Lowering<'_> { |
| 561 | let anchor = next.map(|n| self.id(n)); | 637 | let anchor = next.map(|n| self.id(n)); |
| 562 | if !old.paragraphs.contains_key(id) && !self.alias.contains_key(id) { | 638 | if !old.paragraphs.contains_key(id) && !self.alias.contains_key(id) { |
| 563 | let paragraph = new.paragraphs[id]; | 639 | let paragraph = new.paragraphs[id]; |
| 564 | let text = paragraph | 640 | if paragraph.style.is_some() { |
| 565 | .text() | 641 | return Err(invalid( |
| 566 | .filter(|text| text.date_field.is_none()) | 642 | "New paragraphs contain plain text without fields or styles", |
| 567 | .filter(|_| paragraph.style.is_none()) | 643 | )); |
| 568 | .ok_or_else(|| { | 644 | } |
| 569 | invalid("New paragraphs contain plain text without fields or styles") | 645 | // A new table starts as an empty text paragraph whose content the |
| 570 | })?; | 646 | // structure pass replaces with the table. |
| 571 | let insertion = Insertion::paragraph( | 647 | let (text, text_id) = match &paragraph.content { |
| 572 | self.id(container), | 648 | ParagraphContent::Text(text) if text.date_field.is_none() => { |
| 573 | anchor, | 649 | (text.text.text(), Some(text.id)) |
| 574 | text.text.text(), | 650 | } |
| 575 | self.author, | 651 | ParagraphContent::Table(_) => ("", None), |
| 576 | )?; | 652 | _ => { |
| 653 | return Err(invalid( | ||
| 654 | "New paragraphs contain plain text without fields or styles", | ||
| 655 | )); | ||
| 656 | } | ||
| 657 | }; | ||
| 658 | let insertion = | ||
| 659 | Insertion::paragraph(self.id(*container), anchor, text, self.author)?; | ||
| 577 | let space = self.space; | 660 | let space = self.space; |
| 578 | self.apply(|image| insertion.apply(image, space))?; | 661 | self.apply(|image| insertion.apply(image, space))?; |
| 579 | self.alias.insert(*id, insertion.object()); | 662 | self.alias.insert(*id, insertion.object()); |
| 580 | self.alias.insert(text.id, insertion.text_object()); | 663 | if let Some(text_id) = text_id { |
| 664 | self.alias.insert(text_id, insertion.text_object()); | ||
| 665 | } | ||
| 581 | } else if !kept.contains(id) { | 666 | } else if !kept.contains(id) { |
| 582 | let edit = | 667 | let edit = |
| 583 | TreeEdit::move_to(self.id(*id), self.id(container), anchor, self.author)?; | 668 | TreeEdit::move_to(self.id(*id), self.id(*container), anchor, self.author)?; |
| 584 | let space = self.space; | 669 | let space = self.space; |
| 585 | self.apply(|image| edit.apply(image, space))?; | 670 | self.apply(|image| edit.apply(image, space))?; |
| 586 | } | 671 | } |
| ... | @@ -590,6 +675,187 @@ impl Lowering<'_> { | ... | @@ -590,6 +675,187 @@ impl Lowering<'_> { |
| 590 | Ok(()) | 675 | Ok(()) |
| 591 | } | 676 | } |
| 592 | 677 | ||
| 678 | /// Creates tables, rows and cells the model added, rebuilds every table's row and cell | ||
| 679 | /// order to the model's, and writes the column widths, locks and border flag. Native | ||
| 680 | /// rows and cells are plain containers; a cell carries its indent array and the flags | ||
| 681 | /// every native cell has. | ||
| 682 | fn edit_table_structure(&mut self, old: &View<'_>, new: &View<'_>) -> Result<(), Error> { | ||
| 683 | let old_tables: BTreeMap<ExGuid, &Table> = old | ||
| 684 | .paragraphs | ||
| 685 | .values() | ||
| 686 | .filter_map(|p| match &p.content { | ||
| 687 | ParagraphContent::Table(table) => Some((table.id, table)), | ||
| 688 | _ => None, | ||
| 689 | }) | ||
| 690 | .collect(); | ||
| 691 | for (paragraph_id, paragraph) in &new.paragraphs { | ||
| 692 | let ParagraphContent::Table(table) = &paragraph.content else { | ||
| 693 | continue; | ||
| 694 | }; | ||
| 695 | let previous = old_tables.get(&table.id).copied(); | ||
| 696 | let unchanged = previous.is_some_and(|previous| { | ||
| 697 | previous.columns == table.columns | ||
| 698 | && previous.borders == table.borders | ||
| 699 | && previous.rows.len() == table.rows.len() | ||
| 700 | && previous.rows.iter().zip(&table.rows).all(|(a, b)| { | ||
| 701 | a.id == b.id | ||
| 702 | && a.cells.len() == b.cells.len() | ||
| 703 | && a.cells.iter().zip(&b.cells).all(|(x, y)| x.id == y.id) | ||
| 704 | }) | ||
| 705 | }); | ||
| 706 | if unchanged { | ||
| 707 | continue; | ||
| 708 | } | ||
| 709 | let modified = crate::create::current_timestamps()?.0.to_le_bytes(); | ||
| 710 | let mut created: Vec<(ExGuid, u32, Values)> = Vec::new(); | ||
| 711 | let table_id = if previous.is_some() { | ||
| 712 | self.id(table.id) | ||
| 713 | } else { | ||
| 714 | self.allocate(table.id)? | ||
| 715 | }; | ||
| 716 | let existing_rows: BTreeSet<ExGuid> = previous | ||
| 717 | .map(|p| p.rows.iter().map(|r| r.id).collect()) | ||
| 718 | .unwrap_or_default(); | ||
| 719 | let existing_cells: BTreeSet<ExGuid> = previous | ||
| 720 | .map(|p| { | ||
| 721 | p.rows | ||
| 722 | .iter() | ||
| 723 | .flat_map(|r| r.cells.iter().map(|c| c.id)) | ||
| 724 | .collect() | ||
| 725 | }) | ||
| 726 | .unwrap_or_default(); | ||
| 727 | let template = previous | ||
| 728 | .and_then(|p| p.rows.first()) | ||
| 729 | .and_then(|r| r.cells.first()); | ||
| 730 | let mut rows = Vec::new(); | ||
| 731 | for row in &table.rows { | ||
| 732 | let row_id = if existing_rows.contains(&row.id) { | ||
| 733 | self.id(row.id) | ||
| 734 | } else { | ||
| 735 | self.allocate(row.id)? | ||
| 736 | }; | ||
| 737 | let mut cells = Vec::new(); | ||
| 738 | for cell in &row.cells { | ||
| 739 | let cell_id = if existing_cells.contains(&cell.id) { | ||
| 740 | self.id(cell.id) | ||
| 741 | } else { | ||
| 742 | let written = self.allocate(cell.id)?; | ||
| 743 | let indents = if cell.indents.is_empty() { | ||
| 744 | template.map(|t| t.indents.clone()).unwrap_or_default() | ||
| 745 | } else { | ||
| 746 | cell.indents.clone() | ||
| 747 | }; | ||
| 748 | let mut values: Values = vec![ | ||
| 749 | (0x14001d7a, modified.to_vec()), | ||
| 750 | (0x0c001c13, vec![0]), | ||
| 751 | (0x0c001c03, vec![1]), | ||
| 752 | (0x88001c91, Vec::new()), | ||
| 753 | ]; | ||
| 754 | if !indents.is_empty() { | ||
| 755 | values.push((0x1c001c12, measurement_bytes(&indents, 4)?)); | ||
| 756 | } | ||
| 757 | if let Some(shading) = cell.shading { | ||
| 758 | values.push((0x14001e26, shading.to_le_bytes().to_vec())); | ||
| 759 | } | ||
| 760 | created.push((written, 0x60024, values)); | ||
| 761 | written | ||
| 762 | }; | ||
| 763 | cells.push(cell_id); | ||
| 764 | } | ||
| 765 | if !existing_rows.contains(&row.id) { | ||
| 766 | created.push((row_id, 0x60023, vec![(0x14001d7a, modified.to_vec())])); | ||
| 767 | } | ||
| 768 | rows.push((row_id, cells)); | ||
| 769 | } | ||
| 770 | let mut table_values: Values = vec![ | ||
| 771 | (0x14001d7a, modified.to_vec()), | ||
| 772 | (0x14001d57, (table.rows.len() as u32).to_le_bytes().to_vec()), | ||
| 773 | ( | ||
| 774 | 0x14001d58, | ||
| 775 | (table.columns.len() as u32).to_le_bytes().to_vec(), | ||
| 776 | ), | ||
| 777 | ( | ||
| 778 | 0x1c001d66, | ||
| 779 | measurement_bytes( | ||
| 780 | &table.columns.iter().map(|c| c.width).collect::<Vec<_>>(), | ||
| 781 | 1, | ||
| 782 | )?, | ||
| 783 | ), | ||
| 784 | ]; | ||
| 785 | let mut locks = vec![table.columns.len() as u8]; | ||
| 786 | locks.extend(vec![0u8; table.columns.len().div_ceil(8)]); | ||
| 787 | for (i, column) in table.columns.iter().enumerate() { | ||
| 788 | if column.locked { | ||
| 789 | locks[1 + i / 8] |= 1 << (i % 8); | ||
| 790 | } | ||
| 791 | } | ||
| 792 | table_values.push((0x1c001d7d, locks)); | ||
| 793 | table_values.push(( | ||
| 794 | 0x08001d5e | (u32::from(table.borders.unwrap_or(true)) << 31), | ||
| 795 | Vec::new(), | ||
| 796 | )); | ||
| 797 | if previous.is_none() { | ||
| 798 | table_values.push((0x14001c3e, 1u32.to_le_bytes().to_vec())); | ||
| 799 | table_values.push((0x14001c84, 1u32.to_le_bytes().to_vec())); | ||
| 800 | created.push((table_id, 0x60022, Vec::new())); | ||
| 801 | } | ||
| 802 | let holder = self.id(*paragraph_id); | ||
| 803 | let is_new = previous.is_none(); | ||
| 804 | let space = self.space; | ||
| 805 | self.apply(|image| { | ||
| 806 | crate::write::write_revision(image, space, |raw| { | ||
| 807 | let mut changed: BTreeMap<ExGuid, PropertyObject> = BTreeMap::new(); | ||
| 808 | for (id, jcid, values) in &created { | ||
| 809 | let mut node = PropertyObject { | ||
| 810 | jcid: *jcid, | ||
| 811 | bytes: crate::create::properties(values)?, | ||
| 812 | global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])), | ||
| 813 | }; | ||
| 814 | node.reference(*id)?; | ||
| 815 | changed.insert(*id, node); | ||
| 816 | } | ||
| 817 | let mut row_refs = Vec::new(); | ||
| 818 | for (row_id, cells) in &rows { | ||
| 819 | let mut row = match changed.remove(row_id) { | ||
| 820 | Some(row) => row, | ||
| 821 | None => PropertyObject::from_object(&raw.objects[row_id])?, | ||
| 822 | }; | ||
| 823 | let mut cell_refs = Vec::new(); | ||
| 824 | for cell in cells { | ||
| 825 | cell_refs.extend_from_slice(&row.reference(*cell)?); | ||
| 826 | } | ||
| 827 | row.set(&[(0x24001c20, &cell_refs), (0x14001d7a, &modified)])?; | ||
| 828 | changed.insert(*row_id, row); | ||
| 829 | } | ||
| 830 | let mut table_object = match changed.remove(&table_id) { | ||
| 831 | Some(object) => object, | ||
| 832 | None => PropertyObject::from_object(&raw.objects[&table_id])?, | ||
| 833 | }; | ||
| 834 | for (row_id, _) in &rows { | ||
| 835 | row_refs.extend_from_slice(&table_object.reference(*row_id)?); | ||
| 836 | } | ||
| 837 | table_object.remove(&[0x1c001d7d, 0x08001d5e])?; | ||
| 838 | table_object.set( | ||
| 839 | &table_values | ||
| 840 | .iter() | ||
| 841 | .map(|(id, bytes)| (*id, bytes.as_slice())) | ||
| 842 | .collect::<Vec<_>>(), | ||
| 843 | )?; | ||
| 844 | table_object.set(&[(0x24001c20, &row_refs)])?; | ||
| 845 | changed.insert(table_id, table_object); | ||
| 846 | if is_new { | ||
| 847 | let mut object = PropertyObject::from_object(&raw.objects[&holder])?; | ||
| 848 | let reference = object.reference(table_id)?; | ||
| 849 | object.set(&[(0x24001c1f, &reference), (0x14001d7a, &modified)])?; | ||
| 850 | changed.insert(holder, object); | ||
| 851 | } | ||
| 852 | Ok(changed) | ||
| 853 | }) | ||
| 854 | })?; | ||
| 855 | } | ||
| 856 | Ok(()) | ||
| 857 | } | ||
| 858 | |||
| 593 | fn delete( | 859 | fn delete( |
| 594 | &mut self, | 860 | &mut self, |
| 595 | old: &View<'_>, | 861 | old: &View<'_>, |
| ... | @@ -609,6 +875,10 @@ impl Lowering<'_> { | ... | @@ -609,6 +875,10 @@ impl Lowering<'_> { |
| 609 | || (old.paragraphs.contains_key(&ancestor) | 875 | || (old.paragraphs.contains_key(&ancestor) |
| 610 | && !new.paragraphs.contains_key(&ancestor) | 876 | && !new.paragraphs.contains_key(&ancestor) |
| 611 | && !consumed.contains(&ancestor)) | 877 | && !consumed.contains(&ancestor)) |
| 878 | || (old.children.contains_key(&ancestor) | ||
| 879 | && !old.paragraphs.contains_key(&ancestor) | ||
| 880 | && !old.outlines.contains_key(&ancestor) | ||
| 881 | && !new.children.contains_key(&ancestor)) | ||
| 612 | { | 882 | { |
| 613 | covered = true; | 883 | covered = true; |
| 614 | break; | 884 | break; |
crates/onestore/tests/page_tables.rs created+306| ... | @@ -0,0 +1,306 @@ | ||
| 1 | use onestore::{ | ||
| 2 | ExGuid, PreparedEdit, RevisionIndex, Store, | ||
| 3 | document::{Document, Format}, | ||
| 4 | page::{ | ||
| 5 | Page, PageObject, PageParagraph, ParagraphContent, Table, TableCell, TableColumn, TableRow, | ||
| 6 | TextObject, text::new_id, | ||
| 7 | }, | ||
| 8 | }; | ||
| 9 | |||
| 10 | const TREES: &[u8] = | ||
| 11 | include_bytes!("../../../corpus/outline-edit/tree/before/notebook/synthetic.one"); | ||
| 12 | const AUTHOR: &str = "Table author"; | ||
| 13 | |||
| 14 | fn page_by_title(bytes: &[u8], title: &str) -> (ExGuid, Page) { | ||
| 15 | let store = Store::parse(bytes).unwrap(); | ||
| 16 | assert!(store.checksum_mismatches.is_empty()); | ||
| 17 | let index = RevisionIndex::parse(&store).unwrap(); | ||
| 18 | let document = Document::parse(&index).unwrap(); | ||
| 19 | document | ||
| 20 | .pages() | ||
| 21 | .unwrap() | ||
| 22 | .into_iter() | ||
| 23 | .find_map(|(space, _)| { | ||
| 24 | let page = Page::from_space(&document, space).unwrap(); | ||
| 25 | (page.title == title).then_some((space, page)) | ||
| 26 | }) | ||
| 27 | .unwrap() | ||
| 28 | } | ||
| 29 | |||
| 30 | fn page_in(bytes: &[u8], space: ExGuid) -> Page { | ||
| 31 | let store = Store::parse(bytes).unwrap(); | ||
| 32 | assert!(store.checksum_mismatches.is_empty()); | ||
| 33 | let index = RevisionIndex::parse(&store).unwrap(); | ||
| 34 | Page::from_space(&Document::parse(&index).unwrap(), space).unwrap() | ||
| 35 | } | ||
| 36 | |||
| 37 | fn body_paragraphs(page: &mut Page) -> &mut Vec<PageParagraph> { | ||
| 38 | page.objects | ||
| 39 | .iter_mut() | ||
| 40 | .find_map(|object| match object { | ||
| 41 | PageObject::Outline(outline) if !outline.title => Some(&mut outline.paragraphs), | ||
| 42 | _ => None, | ||
| 43 | }) | ||
| 44 | .unwrap() | ||
| 45 | } | ||
| 46 | |||
| 47 | fn table_mut(page: &mut Page) -> &mut Table { | ||
| 48 | body_paragraphs(page) | ||
| 49 | .iter_mut() | ||
| 50 | .find_map(|p| match &mut p.content { | ||
| 51 | ParagraphContent::Table(table) => Some(table), | ||
| 52 | _ => None, | ||
| 53 | }) | ||
| 54 | .unwrap() | ||
| 55 | } | ||
| 56 | |||
| 57 | /// Definitions are read back only while a paragraph references them. | ||
| 58 | fn referenced(page: &Page) -> std::collections::BTreeSet<ExGuid> { | ||
| 59 | fn walk(paragraphs: &[PageParagraph], out: &mut std::collections::BTreeSet<ExGuid>) { | ||
| 60 | for paragraph in paragraphs { | ||
| 61 | out.extend(paragraph.lists.iter().copied()); | ||
| 62 | out.extend(paragraph.style); | ||
| 63 | out.extend(paragraph.tags.iter().filter_map(|t| t.definition)); | ||
| 64 | match &paragraph.content { | ||
| 65 | ParagraphContent::Text(text) => { | ||
| 66 | out.extend(text.tags.iter().filter_map(|t| t.definition)); | ||
| 67 | } | ||
| 68 | ParagraphContent::Table(table) => { | ||
| 69 | for row in &table.rows { | ||
| 70 | for cell in &row.cells { | ||
| 71 | walk(&cell.paragraphs, out); | ||
| 72 | } | ||
| 73 | } | ||
| 74 | } | ||
| 75 | ParagraphContent::Unsupported(_) => {} | ||
| 76 | } | ||
| 77 | } | ||
| 78 | } | ||
| 79 | let mut out = std::collections::BTreeSet::new(); | ||
| 80 | for object in &page.objects { | ||
| 81 | match object { | ||
| 82 | PageObject::Outline(outline) => walk(&outline.paragraphs, &mut out), | ||
| 83 | PageObject::Title(title) => { | ||
| 84 | for outline in &title.outlines { | ||
| 85 | walk(&outline.paragraphs, &mut out); | ||
| 86 | } | ||
| 87 | } | ||
| 88 | _ => {} | ||
| 89 | } | ||
| 90 | } | ||
| 91 | out | ||
| 92 | } | ||
| 93 | |||
| 94 | fn assert_same(written: &[u8], space: ExGuid, expected: &Page) -> Page { | ||
| 95 | let stored = page_in(written, space); | ||
| 96 | let mut expected = expected.clone(); | ||
| 97 | expected.title = stored.title.clone(); | ||
| 98 | let live = referenced(&expected); | ||
| 99 | expected.definitions.retain(|id, _| live.contains(id)); | ||
| 100 | assert_eq!(stored, expected); | ||
| 101 | stored | ||
| 102 | } | ||
| 103 | |||
| 104 | fn plain_text(text: &str) -> TextObject { | ||
| 105 | TextObject { | ||
| 106 | id: new_id().unwrap(), | ||
| 107 | date_field: None, | ||
| 108 | text: onestore::page::Paragraph::new( | ||
| 109 | text.into(), | ||
| 110 | Format { | ||
| 111 | font: Some("Calibri".into()), | ||
| 112 | font_size: Some(11.0), | ||
| 113 | language: Some(1033), | ||
| 114 | ..Default::default() | ||
| 115 | }, | ||
| 116 | ), | ||
| 117 | tags: Vec::new(), | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | fn cell_paragraph(template: &PageParagraph, text: &str) -> PageParagraph { | ||
| 122 | let mut paragraph = template.clone(); | ||
| 123 | paragraph.id = new_id().unwrap(); | ||
| 124 | paragraph.parent = None; | ||
| 125 | paragraph.level = 1; | ||
| 126 | paragraph.lists.clear(); | ||
| 127 | paragraph.tags.clear(); | ||
| 128 | paragraph.style = None; | ||
| 129 | paragraph.collapsed = false; | ||
| 130 | paragraph.content = ParagraphContent::Text(plain_text(text)); | ||
| 131 | paragraph | ||
| 132 | } | ||
| 133 | |||
| 134 | fn new_cell(template: &TableCell, text: &str) -> TableCell { | ||
| 135 | TableCell { | ||
| 136 | id: new_id().unwrap(), | ||
| 137 | layout: Default::default(), | ||
| 138 | indents: template.indents.clone(), | ||
| 139 | shading: None, | ||
| 140 | paragraphs: vec![cell_paragraph(&template.paragraphs[0], text)], | ||
| 141 | unsupported: Vec::new(), | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 145 | #[test] | ||
| 146 | fn a_row_and_a_column_are_added_to_a_native_table_and_removed_again() { | ||
| 147 | let (space, before) = page_by_title(TREES, "Delete cell subtree"); | ||
| 148 | let mut after = before.clone(); | ||
| 149 | let table = table_mut(&mut after); | ||
| 150 | assert_eq!((table.rows.len(), table.columns.len()), (1, 2)); | ||
| 151 | let template = table.rows[0].cells[0].clone(); | ||
| 152 | table.columns.push(TableColumn { | ||
| 153 | width: 120.0, | ||
| 154 | locked: false, | ||
| 155 | }); | ||
| 156 | table.rows[0] | ||
| 157 | .cells | ||
| 158 | .push(new_cell(&template, "Third column")); | ||
| 159 | table.rows.push(TableRow { | ||
| 160 | id: new_id().unwrap(), | ||
| 161 | cells: vec![ | ||
| 162 | new_cell(&template, "Second row"), | ||
| 163 | new_cell(&template, "Second row, second column"), | ||
| 164 | new_cell(&template, "Second row, third column"), | ||
| 165 | ], | ||
| 166 | }); | ||
| 167 | let written = PreparedEdit::page(TREES, space, &after, AUTHOR).unwrap(); | ||
| 168 | let stored = assert_same(written.as_bytes(), space, &after); | ||
| 169 | assert_eq!( | ||
| 170 | PreparedEdit::page(written.as_bytes(), space, &stored, AUTHOR) | ||
| 171 | .unwrap() | ||
| 172 | .as_bytes(), | ||
| 173 | written.as_bytes() | ||
| 174 | ); | ||
| 175 | if let Some(directory) = std::env::var_os("ONESTORE_TABLE_EDIT_EXPORT") { | ||
| 176 | let directory = std::path::PathBuf::from(directory); | ||
| 177 | std::fs::create_dir(&directory).unwrap(); | ||
| 178 | std::fs::write(directory.join("synthetic.one"), written.as_bytes()).unwrap(); | ||
| 179 | let written_store = Store::parse(written.as_bytes()).unwrap(); | ||
| 180 | std::fs::write( | ||
| 181 | directory.join("Open Notebook.onetoc2"), | ||
| 182 | onestore::create_table_of_contents( | ||
| 183 | "Open Notebook.onetoc2", | ||
| 184 | &[("synthetic.one", written_store.header.file_id)], | ||
| 185 | ) | ||
| 186 | .unwrap(), | ||
| 187 | ) | ||
| 188 | .unwrap(); | ||
| 189 | } | ||
| 190 | let mut smaller = stored.clone(); | ||
| 191 | let table = table_mut(&mut smaller); | ||
| 192 | table.columns.remove(1); | ||
| 193 | for row in &mut table.rows { | ||
| 194 | row.cells.remove(1); | ||
| 195 | } | ||
| 196 | table.rows.remove(0); | ||
| 197 | let again = PreparedEdit::page(written.as_bytes(), space, &smaller, AUTHOR).unwrap(); | ||
| 198 | let mut stored = assert_same(again.as_bytes(), space, &smaller); | ||
| 199 | let table = table_mut(&mut stored); | ||
| 200 | assert_eq!((table.rows.len(), table.columns.len()), (1, 2)); | ||
| 201 | } | ||
| 202 | |||
| 203 | #[test] | ||
| 204 | fn column_widths_locks_and_borders_change_in_place() { | ||
| 205 | let (space, before) = page_by_title(TREES, "Delete cell subtree"); | ||
| 206 | let mut after = before.clone(); | ||
| 207 | let table = table_mut(&mut after); | ||
| 208 | table.columns[0].width = 300.0; | ||
| 209 | table.columns[1].locked = false; | ||
| 210 | table.borders = Some(false); | ||
| 211 | let written = PreparedEdit::page(TREES, space, &after, AUTHOR).unwrap(); | ||
| 212 | assert_same(written.as_bytes(), space, &after); | ||
| 213 | } | ||
| 214 | |||
| 215 | #[test] | ||
| 216 | fn inconsistent_tables_are_refused() { | ||
| 217 | let (space, before) = page_by_title(TREES, "Delete cell subtree"); | ||
| 218 | let mut ragged = before.clone(); | ||
| 219 | let template = table_mut(&mut ragged).rows[0].cells[0].clone(); | ||
| 220 | table_mut(&mut ragged).rows[0] | ||
| 221 | .cells | ||
| 222 | .push(new_cell(&template, "Extra")); | ||
| 223 | assert!(PreparedEdit::page(TREES, space, &ragged, AUTHOR).is_err()); | ||
| 224 | let mut empty = before.clone(); | ||
| 225 | table_mut(&mut empty).rows[0].cells[0].paragraphs.clear(); | ||
| 226 | let written = PreparedEdit::page(TREES, space, &empty, AUTHOR).unwrap(); | ||
| 227 | let mut stored = page_in(written.as_bytes(), space); | ||
| 228 | let cell = &table_mut(&mut stored).rows[0].cells[0]; | ||
| 229 | assert_eq!(cell.paragraphs.len(), 1); | ||
| 230 | assert_eq!(cell.paragraphs[0].text().unwrap().text.text(), ""); | ||
| 231 | let mut narrow = before.clone(); | ||
| 232 | table_mut(&mut narrow).columns[0].width = 10.0; | ||
| 233 | assert!(PreparedEdit::page(TREES, space, &narrow, AUTHOR).is_err()); | ||
| 234 | let mut rowless = before.clone(); | ||
| 235 | table_mut(&mut rowless).rows.clear(); | ||
| 236 | assert!(PreparedEdit::page(TREES, space, &rowless, AUTHOR).is_err()); | ||
| 237 | } | ||
| 238 | |||
| 239 | /// `ONESTORE_TABLE_EXPORT` names a new directory receiving the candidate for a cold reopen. | ||
| 240 | #[test] | ||
| 241 | fn a_new_table_is_created_on_a_fresh_page() { | ||
| 242 | let source = onestore::create_section("tables.one", "Before the table", "Author").unwrap(); | ||
| 243 | let store = Store::parse(&source).unwrap(); | ||
| 244 | let index = RevisionIndex::parse(&store).unwrap(); | ||
| 245 | let document = Document::parse(&index).unwrap(); | ||
| 246 | let (space, _) = document.pages().unwrap()[0]; | ||
| 247 | let before = Page::from_space(&document, space).unwrap(); | ||
| 248 | let mut after = before.clone(); | ||
| 249 | let template = body_paragraphs(&mut after)[0].clone(); | ||
| 250 | let cell = |text: &str| TableCell { | ||
| 251 | id: new_id().unwrap(), | ||
| 252 | layout: Default::default(), | ||
| 253 | indents: Vec::new(), | ||
| 254 | shading: None, | ||
| 255 | paragraphs: vec![cell_paragraph(&template, text)], | ||
| 256 | unsupported: Vec::new(), | ||
| 257 | }; | ||
| 258 | let table = Table { | ||
| 259 | id: new_id().unwrap(), | ||
| 260 | columns: vec![ | ||
| 261 | TableColumn { | ||
| 262 | width: 144.0, | ||
| 263 | locked: true, | ||
| 264 | }, | ||
| 265 | TableColumn { | ||
| 266 | width: 216.0, | ||
| 267 | locked: true, | ||
| 268 | }, | ||
| 269 | ], | ||
| 270 | rows: vec![ | ||
| 271 | TableRow { | ||
| 272 | id: new_id().unwrap(), | ||
| 273 | cells: vec![cell("Name"), cell("Value")], | ||
| 274 | }, | ||
| 275 | TableRow { | ||
| 276 | id: new_id().unwrap(), | ||
| 277 | cells: vec![cell("Rust 🦀"), cell("Written natively")], | ||
| 278 | }, | ||
| 279 | ], | ||
| 280 | borders: Some(true), | ||
| 281 | layout: Default::default(), | ||
| 282 | tags: Vec::new(), | ||
| 283 | }; | ||
| 284 | let mut holder = cell_paragraph(&template, ""); | ||
| 285 | holder.content = ParagraphContent::Table(table); | ||
| 286 | holder.format = Format::default(); | ||
| 287 | body_paragraphs(&mut after).push(holder); | ||
| 288 | body_paragraphs(&mut after).push(cell_paragraph(&template, "After the table")); | ||
| 289 | let written = PreparedEdit::page(&source, space, &after, AUTHOR).unwrap(); | ||
| 290 | assert_same(written.as_bytes(), space, &after); | ||
| 291 | if let Some(directory) = std::env::var_os("ONESTORE_TABLE_EXPORT") { | ||
| 292 | let directory = std::path::PathBuf::from(directory); | ||
| 293 | std::fs::create_dir(&directory).unwrap(); | ||
| 294 | std::fs::write(directory.join("tables.one"), written.as_bytes()).unwrap(); | ||
| 295 | let written_store = Store::parse(written.as_bytes()).unwrap(); | ||
| 296 | std::fs::write( | ||
| 297 | directory.join("Open Notebook.onetoc2"), | ||
| 298 | onestore::create_table_of_contents( | ||
| 299 | "Open Notebook.onetoc2", | ||
| 300 | &[("tables.one", written_store.header.file_id)], | ||
| 301 | ) | ||
| 302 | .unwrap(), | ||
| 303 | ) | ||
| 304 | .unwrap(); | ||
| 305 | } | ||
| 306 | } | ||
crates/onestore/tests/page_writer.rs+1-1| ... | @@ -604,7 +604,7 @@ fn unsupported_model_edits_are_rejected_before_writing() { | ... | @@ -604,7 +604,7 @@ fn unsupported_model_edits_are_rejected_before_writing() { |
| 604 | } | 604 | } |
| 605 | } | 605 | } |
| 606 | } | 606 | } |
| 607 | assert!(PreparedEdit::page(TREES, space, &widened, AUTHOR).is_err()); | 607 | assert!(PreparedEdit::page(TREES, space, &widened, AUTHOR).is_ok()); |
| 608 | let mut relisted = page_in(TREES, space); | 608 | let mut relisted = page_in(TREES, space); |
| 609 | let outline = body(&page)[0]; | 609 | let outline = body(&page)[0]; |
| 610 | for object in &mut relisted.objects { | 610 | for object in &mut relisted.objects { |
tools/test_table_edit.py created+58| ... | @@ -0,0 +1,58 @@ | ||
| 1 | import html | ||
| 2 | from pathlib import Path | ||
| 3 | import re | ||
| 4 | import runpy | ||
| 5 | import shutil | ||
| 6 | from tempfile import TemporaryDirectory | ||
| 7 | import unittest | ||
| 8 | import xml.etree.ElementTree as ET | ||
| 9 | |||
| 10 | from native_xml import ns | ||
| 11 | |||
| 12 | ROOT = Path(__file__).resolve().parent.parent | ||
| 13 | FIXTURE = ROOT / 'corpus/table-edit' | ||
| 14 | compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] | ||
| 15 | |||
| 16 | |||
| 17 | def tables(read, title): | ||
| 18 | for path in sorted(read.glob('page-*.xml')): | ||
| 19 | page = ET.parse(path).getroot() | ||
| 20 | if page.get('name') != title: | ||
| 21 | continue | ||
| 22 | out = [] | ||
| 23 | for table in page.iter('{%s}Table' % ns['one']): | ||
| 24 | columns = [(column.get('width'), column.get('isLocked')) for column in table.find('one:Columns', ns)] | ||
| 25 | rows = [[[html.unescape(re.sub(r'<[^>]*>', '', text.text or '')) for text in cell.findall('.//one:T', ns)] | ||
| 26 | for cell in row.findall('one:Cell', ns)] for row in table.findall('one:Row', ns)] | ||
| 27 | out.append((table.get('bordersVisible'), columns, rows)) | ||
| 28 | return out | ||
| 29 | raise AssertionError(title) | ||
| 30 | |||
| 31 | |||
| 32 | class TableEditTest(unittest.TestCase): | ||
| 33 | def check(self, name): | ||
| 34 | with TemporaryDirectory() as temporary: | ||
| 35 | read = Path(temporary) / 'read' | ||
| 36 | shutil.copytree(FIXTURE / name / 'cold/read', read) | ||
| 37 | compare(FIXTURE / name / 'candidate', read) | ||
| 38 | |||
| 39 | def test_a_rust_table_renders_natively(self): | ||
| 40 | self.check('created') | ||
| 41 | self.assertEqual(tables(FIXTURE / 'created/cold/read', 'Before the table'), [ | ||
| 42 | ('true', [('144.0', 'true'), ('216.0', 'true')], | ||
| 43 | [[['Name'], ['Value']], [['Rust 🦀'], ['Written natively']]]), | ||
| 44 | ]) | ||
| 45 | |||
| 46 | def test_rows_and_columns_added_to_a_native_table_render_natively(self): | ||
| 47 | self.check('edited') | ||
| 48 | (borders, columns, rows), = tables(FIXTURE / 'edited/cold/read', 'Delete cell subtree') | ||
| 49 | self.assertEqual(borders, 'true') | ||
| 50 | self.assertEqual(columns[:2], [('240.0', 'true'), ('180.0', 'true')]) | ||
| 51 | self.assertEqual(len(columns), 3) | ||
| 52 | self.assertIsNone(columns[2][1]) | ||
| 53 | self.assertEqual(rows[0][2], ['Third column']) | ||
| 54 | self.assertEqual(rows[1], [['Second row'], ['Second row, second column'], ['Second row, third column']]) | ||
| 55 | |||
| 56 | |||
| 57 | if __name__ == '__main__': | ||
| 58 | unittest.main() | ||