authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-12 12:16:00-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-09-12 17:00:32-07:00
log59c8648048632812f85fd595c05aa9720ad2a051
tree2bad0c7aaf539ac8292d94301a97f32d68d6c791
parent8b4f45b5b7a6d9b8555e7234a123c9887495d265
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: write table rows, columns and new tables through the page model

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.1

48 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
6paragraph between two text paragraphs becomes a two-by-two table with locked
7144 pt and 216 pt columns, visible borders and a text paragraph in every
8cell. `created/cold` is a fresh OneNote 2010 read showing both columns with
9their 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
15of three cells. `edited/cold` is its cold read: the native columns keep their
16widths and locks, OneNote sizes the unlocked column to its content, and the
17new row and cells read back with their text.
18
19Native rows and cells are plain containers; the writer creates them with the
20modification time, the indent array copied from a sibling cell, and the flags
21every native cell carries, rebuilds each table's row and cell order to the
22model's, and writes the column count, widths, locks and border flag. New
23tables start as an empty paragraph whose content the structure pass replaces.
24An 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
27directories 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 @@
1param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '')
2Set-StrictMode -Version Latest
3$ErrorActionPreference = 'Stop'
4$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
5if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
6 throw 'Choose a run directly below C:\one-tests\runs.'
7}
8if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' }
9$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
10if ($CloneHost) {
11 if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) {
12 throw 'The disposable clone hostname does not match this machine.'
13 }
14 New-Item "$key\Options\Paths" -Force | Out-Null
15 New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null
16} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
17 -not (Test-Path 'C:\one-tests\profile-original-cache')) {
18 throw 'Park the personal OneNote profile before resetting the test cache.'
19}
20$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0'
21$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root))
22if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' }
23New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null
24if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked }
25if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse }
26New-Item "$key\OpenNotebooks" | Out-Null
27New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null
corpus/table-edit/created/cold/scripts/read.ps1 created+142
......@@ -0,0 +1,142 @@
1param(
2 [Parameter(Mandatory=$true)][string]$Root,
3 [int]$ExpectedPages = -1,
4 [switch]$UseCurrentCache,
5 [switch]$Pdf,
6 [switch]$KeepOpen,
7 [string]$CloneHost = ''
8)
9Set-StrictMode -Version Latest
10$ErrorActionPreference = 'Stop'
11$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
12if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
13 throw 'Choose a run directly below C:\one-tests\runs.'
14}
15$notebook = Join-Path $root 'notebook'
16$output = Join-Path $root 'read'
17if (Test-Path $output) { throw 'Choose a new read destination.' }
18if ($UseCurrentCache) {
19 if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
20 throw 'Park the personal OneNote profile before reading test notebooks.'
21 }
22} else {
23 & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
24}
25New-Item -ItemType Directory -Path $output | Out-Null
26$app = New-Object -ComObject OneNote.Application
27$notebookId = ''
28$failure = $null
29try {
30 $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
31 $process = Get-Process ONENOTE
32 @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
33 powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
34 ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
35 $sections = @()
36 foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
37 $id = ''
38 $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
39 $sections += $id
40 }
41 $deadline = [DateTime]::UtcNow.AddSeconds(300)
42 $previous = ''
43 $lastChange = ''
44 $stableSince = [DateTime]::UtcNow
45 $settled = $false
46 do {
47 $pages = @{}
48 foreach ($section in $sections) {
49 $hierarchy = ''
50 $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
51 [xml]$xml = $hierarchy
52 foreach ($node in $xml.SelectNodes('//*[@path]')) {
53 if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
54 throw 'OneNote opened a section outside the copied notebook.'
55 }
56 }
57 foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
58 $id = $node.GetAttribute('ID')
59 $content = ''
60 $app.GetPageContent($id, [ref]$content, 1, 1)
61 $pages[$id] = $content
62 }
63 }
64 $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
65 if ($signature -ne $previous) {
66 $lastChange = $previous
67 $previous = $signature
68 $stableSince = [DateTime]::UtcNow
69 }
70 if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
71 ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
72 ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
73 Start-Sleep -Milliseconds 250
74 } while ([DateTime]::UtcNow -lt $deadline)
75 if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
76 [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8)
77 $index = 0
78 foreach ($id in @($pages.Keys | Sort-Object)) {
79 [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
80 $index++
81 }
82 $hierarchy = ''
83 $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
84 [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
85 throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled."
86 }
87 $index = 0
88 $payloads = @()
89 foreach ($id in @($pages.Keys | Sort-Object)) {
90 [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
91 if ($Pdf) {
92 $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
93 $app.NavigateTo($id, '', $false)
94 $app.Publish($id, $pdfPath, 3, '')
95 if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
96 }
97 [xml]$page = $pages[$id]
98 foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
99 $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
100 $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
101 [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
102 $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID');
103 kind = $file.LocalName; name = $file.GetAttribute('preferredName');
104 sha256 = $hash; bytes = $bytes.Length }
105 }
106 $index++
107 }
108 [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
109 $all = ''
110 $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
111 [xml]$finalTree = $all
112 $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
113 if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
114 throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
115 }
116 [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
117 Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
118} catch {
119 $failure = $_
120 [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
121 throw
122} finally {
123 try {
124 try {
125 if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
126 if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
127 $app.CloseNotebook($notebookId, $false)
128 }
129 } catch {
130 if ($null -eq $failure) { throw }
131 [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
132 }
133 } finally {
134 [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
135 }
136 $app = $null
137 [GC]::Collect()
138 [GC]::WaitForPendingFinalizers()
139 if (-not $UseCurrentCache -and -not $CloneHost) {
140 Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
141 }
142}
corpus/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
3style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a
4href="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
5style='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
3style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a
4href="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
5style='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
3style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a
4href="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
5style='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
3style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a
4href="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
5style='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
3style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a
4href="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
5style='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;
3text-decoration:underline;font-family:Calibri;font-size:11.0pt'>Bold</span><span
4style='font-family:Calibri;font-size:11.0pt'> </span><span style='font-family:
5Arial;font-size:13.5pt'>&#129408;</span><span style='font-family:Calibri;
6font-size:11.0pt'> </span><span style='font-style:italic;text-decoration:line-through;
7font-family:Calibri;font-size:11.0pt'>italic</span><span style='font-family:
8Calibri;font-size:11.0pt'> </span><span style='vertical-align:super;font-family:
9Calibri;font-size:11.0pt'>é</span><span style='font-family:Calibri;font-size:
1011.0pt'> </span><span style='font-family:Calibri;font-size:11.0pt;color:#0C2238;
11background:yellow;mso-highlight:yellow'>color</span><span style='font-family:
12Calibri;font-size:11.0pt'> </span><span style='vertical-align:sub;font-family:
13Calibri;font-size:11.0pt'>東京</span><span style='font-family:Calibri;font-size:
1411.0pt'><br>
15End</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
16style='font-weight:bold;font-family:Calibri' lang=en-US>Fictitious: café, </span><span
17style='font-weight:bold;font-family:SimSun' lang=en-US>東京</span><span
18style='font-weight:bold;font-family:Calibri' lang=en-US>, </span><span
19style='font-weight:bold;font-family:Arial;direction:rtl;unicode-bidi:embed'
20lang=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
3style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a
4href="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
3style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a
4href="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
5style='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
3style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a
4href="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
3style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a
4href="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
5style='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
3style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a
4href="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
5style='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
3style='font-weight:bold' lang=en-US>Target &#129408; é</span><span lang=en-US> </span><a
4href="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
5style='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 @@
1param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '')
2Set-StrictMode -Version Latest
3$ErrorActionPreference = 'Stop'
4$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
5if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
6 throw 'Choose a run directly below C:\one-tests\runs.'
7}
8if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' }
9$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote'
10if ($CloneHost) {
11 if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) {
12 throw 'The disposable clone hostname does not match this machine.'
13 }
14 New-Item "$key\Options\Paths" -Force | Out-Null
15 New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null
16} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or
17 -not (Test-Path 'C:\one-tests\profile-original-cache')) {
18 throw 'Park the personal OneNote profile before resetting the test cache.'
19}
20$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0'
21$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root))
22if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' }
23New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null
24if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked }
25if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse }
26New-Item "$key\OpenNotebooks" | Out-Null
27New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null
corpus/table-edit/edited/cold/scripts/read.ps1 created+142
......@@ -0,0 +1,142 @@
1param(
2 [Parameter(Mandatory=$true)][string]$Root,
3 [int]$ExpectedPages = -1,
4 [switch]$UseCurrentCache,
5 [switch]$Pdf,
6 [switch]$KeepOpen,
7 [string]$CloneHost = ''
8)
9Set-StrictMode -Version Latest
10$ErrorActionPreference = 'Stop'
11$root = [IO.Path]::GetFullPath($Root).TrimEnd('\')
12if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') {
13 throw 'Choose a run directly below C:\one-tests\runs.'
14}
15$notebook = Join-Path $root 'notebook'
16$output = Join-Path $root 'read'
17if (Test-Path $output) { throw 'Choose a new read destination.' }
18if ($UseCurrentCache) {
19 if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') {
20 throw 'Park the personal OneNote profile before reading test notebooks.'
21 }
22} else {
23 & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost
24}
25New-Item -ItemType Directory -Path $output | Out-Null
26$app = New-Object -ComObject OneNote.Application
27$notebookId = ''
28$failure = $null
29try {
30 $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0)
31 $process = Get-Process ONENOTE
32 @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion;
33 powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } |
34 ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8
35 $sections = @()
36 foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) {
37 $id = ''
38 $app.OpenHierarchy($file.FullName, '', [ref]$id, 0)
39 $sections += $id
40 }
41 $deadline = [DateTime]::UtcNow.AddSeconds(300)
42 $previous = ''
43 $lastChange = ''
44 $stableSince = [DateTime]::UtcNow
45 $settled = $false
46 do {
47 $pages = @{}
48 foreach ($section in $sections) {
49 $hierarchy = ''
50 $app.GetHierarchy($section, 4, [ref]$hierarchy, 1)
51 [xml]$xml = $hierarchy
52 foreach ($node in $xml.SelectNodes('//*[@path]')) {
53 if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) {
54 throw 'OneNote opened a section outside the copied notebook.'
55 }
56 }
57 foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) {
58 $id = $node.GetAttribute('ID')
59 $content = ''
60 $app.GetPageContent($id, [ref]$content, 1, 1)
61 $pages[$id] = $content
62 }
63 }
64 $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] }))
65 if ($signature -ne $previous) {
66 $lastChange = $previous
67 $previous = $signature
68 $stableSince = [DateTime]::UtcNow
69 }
70 if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or
71 ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and
72 ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break }
73 Start-Sleep -Milliseconds 250
74 } while ([DateTime]::UtcNow -lt $deadline)
75 if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) {
76 [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8)
77 $index = 0
78 foreach ($id in @($pages.Keys | Sort-Object)) {
79 [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
80 $index++
81 }
82 $hierarchy = ''
83 $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1)
84 [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8)
85 throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled."
86 }
87 $index = 0
88 $payloads = @()
89 foreach ($id in @($pages.Keys | Sort-Object)) {
90 [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8)
91 if ($Pdf) {
92 $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index)
93 $app.NavigateTo($id, '', $false)
94 $app.Publish($id, $pdfPath, 3, '')
95 if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' }
96 }
97 [xml]$page = $pages[$id]
98 foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) {
99 $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache'))
100 $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant()
101 [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes)
102 $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID');
103 kind = $file.LocalName; name = $file.GetAttribute('preferredName');
104 sha256 = $hash; bytes = $bytes.Length }
105 }
106 $index++
107 }
108 [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8)
109 $all = ''
110 $app.GetHierarchy($notebookId, 4, [ref]$all, 1)
111 [xml]$finalTree = $all
112 $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique)
113 if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) {
114 throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.'
115 }
116 [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8)
117 Write-Output "Read $($sections.Count) sections and $($pages.Count) pages."
118} catch {
119 $failure = $_
120 [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
121 throw
122} finally {
123 try {
124 try {
125 if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) }
126 if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) {
127 $app.CloseNotebook($notebookId, $false)
128 }
129 } catch {
130 if ($null -eq $failure) { throw }
131 [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8)
132 }
133 } finally {
134 [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
135 }
136 $app = $null
137 [GC]::Collect()
138 [GC]::WaitForPendingFinalizers()
139 if (-not $UseCurrentCache -and -not $CloneHost) {
140 Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10
141 }
142}
corpus/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,
1414writes the difference into the working image and queues one
1515`Operation::Page(PageIntent { before, after, author })`; `before` is the page the
1616edit started from and is the precondition reconciliation checks. Text, styles,
17paragraph formatting, hyperlinks, bullets, numbering and note tags, paragraph structure,
17paragraph formatting, hyperlinks, bullets, numbering, note tags, table rows and columns, paragraph structure,
1818outline layout, insertions and deletions are all differences between `before` and
1919`after`; the library never sees editor operations.
2020
crates/onestore/README.md+1-1
......@@ -57,7 +57,7 @@ harness also accepts `--client-profile release`.
5757| `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules |
5858| `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs |
5959| `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 |
6161| `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically |
6262| `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter |
6363| `read_file` | Read a snapshot under whole-file exclusion |
crates/onestore/src/page/write.rs+313-43
......@@ -1,7 +1,7 @@
11//! Publishes an edited page model by lowering the difference from the stored page onto
22//! the typed writers, then squashing their transactions into one revision per space.
33
4use super::{Outline, Page, PageObject, PageParagraph, ParagraphContent};
4use super::{Outline, Page, PageObject, PageParagraph, ParagraphContent, Table};
55use crate::{
66 Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets,
77 RevisionIndex, Store, TextAttribute, TreeEdit, Value,
......@@ -20,6 +20,50 @@ fn invalid(message: &'static str) -> Error {
2020 Error { offset: 0, message }
2121}
2222
23/// A native measurement array: a count byte, `header - 1` reserved bytes, then each value
24/// in inches.
25fn 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.
40fn 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
2367pub(crate) fn write_page(
2468 source: &[u8],
2569 space: ExGuid,
......@@ -154,6 +198,16 @@ impl Lowering<'_> {
154198 self.alias.get(&model).copied().unwrap_or(model)
155199 }
156200
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
157211 fn apply(&mut self, edit: impl FnOnce(&[u8]) -> Result<Vec<u8>, Error>) -> Result<(), Error> {
158212 self.image = edit(&self.image)?;
159213 Ok(())
......@@ -187,6 +241,11 @@ impl Lowering<'_> {
187241 }
188242 let old = View::new(before)?;
189243 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 }
190249 self.check_fixed_objects(before, after, &old, &new)?;
191250 for id in new.outlines.keys().chain(new.paragraphs.keys()) {
192251 if !old.outlines.contains_key(id)
......@@ -295,31 +354,28 @@ impl Lowering<'_> {
295354 }
296355 }
297356 (ParagraphContent::Table(table), ParagraphContent::Table(previous)) => {
298 let same = table.id == previous.id
299 && table.columns == previous.columns
300 && table.borders == previous.borders
301 && table.layout == previous.layout
302 && table.tags == previous.tags
303 && table.rows.len() == previous.rows.len()
304 && table
305 .rows
306 .iter()
307 .zip(&previous.rows)
308 .all(|(row, previous)| {
309 row.id == previous.id
310 && row.cells.len() == previous.cells.len()
311 && row.cells.iter().zip(&previous.cells).all(
312 |(cell, previous)| {
313 cell.id == previous.id
314 && cell.layout == previous.layout
315 && cell.indents == previous.indents
316 && cell.shading == previous.shading
317 && cell.unsupported == previous.unsupported
318 },
319 )
320 });
321 if !same {
322 return Err(invalid("Table structure cannot be edited"));
357 if table.id != previous.id
358 || table.layout != previous.layout
359 || table.tags != previous.tags
360 {
361 return Err(invalid("Table identity, layout and tags cannot be edited"));
362 }
363 let unchanged_cells =
364 table.rows.iter().flat_map(|row| &row.cells).all(|cell| {
365 previous
366 .rows
367 .iter()
368 .flat_map(|row| &row.cells)
369 .find(|before| before.id == cell.id)
370 .is_none_or(|before| {
371 cell.layout == before.layout
372 && cell.indents == before.indents
373 && cell.shading == before.shading
374 && cell.unsupported == before.unsupported
375 })
376 });
377 if !unchanged_cells {
378 return Err(invalid("Cell layout, indents and shading cannot be edited"));
323379 }
324380 }
325381 (ParagraphContent::Unsupported(a), ParagraphContent::Unsupported(b)) if a == b => {}
......@@ -544,10 +600,30 @@ impl Lowering<'_> {
544600 collect_containers(&outline.paragraphs, &mut containers);
545601 }
546602 }
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> {
547623 for container in containers {
548 let after = &new.children[&container];
624 let after = &new.children[container];
549625 let current: Vec<ExGuid> = placed
550 .get(&container)
626 .get(container)
551627 .map(|list| {
552628 list.iter()
553629 .copied()
......@@ -561,26 +637,35 @@ impl Lowering<'_> {
561637 let anchor = next.map(|n| self.id(n));
562638 if !old.paragraphs.contains_key(id) && !self.alias.contains_key(id) {
563639 let paragraph = new.paragraphs[id];
564 let text = paragraph
565 .text()
566 .filter(|text| text.date_field.is_none())
567 .filter(|_| paragraph.style.is_none())
568 .ok_or_else(|| {
569 invalid("New paragraphs contain plain text without fields or styles")
570 })?;
571 let insertion = Insertion::paragraph(
572 self.id(container),
573 anchor,
574 text.text.text(),
575 self.author,
576 )?;
640 if paragraph.style.is_some() {
641 return Err(invalid(
642 "New paragraphs contain plain text without fields or styles",
643 ));
644 }
645 // A new table starts as an empty text paragraph whose content the
646 // structure pass replaces with the table.
647 let (text, text_id) = match &paragraph.content {
648 ParagraphContent::Text(text) if text.date_field.is_none() => {
649 (text.text.text(), Some(text.id))
650 }
651 ParagraphContent::Table(_) => ("", None),
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)?;
577660 let space = self.space;
578661 self.apply(|image| insertion.apply(image, space))?;
579662 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 }
581666 } else if !kept.contains(id) {
582667 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)?;
584669 let space = self.space;
585670 self.apply(|image| edit.apply(image, space))?;
586671 }
......@@ -590,6 +675,187 @@ impl Lowering<'_> {
590675 Ok(())
591676 }
592677
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
593859 fn delete(
594860 &mut self,
595861 old: &View<'_>,
......@@ -609,6 +875,10 @@ impl Lowering<'_> {
609875 || (old.paragraphs.contains_key(&ancestor)
610876 && !new.paragraphs.contains_key(&ancestor)
611877 && !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))
612882 {
613883 covered = true;
614884 break;
crates/onestore/tests/page_tables.rs created+306
......@@ -0,0 +1,306 @@
1use 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
10const TREES: &[u8] =
11 include_bytes!("../../../corpus/outline-edit/tree/before/notebook/synthetic.one");
12const AUTHOR: &str = "Table author";
13
14fn 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
30fn 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
37fn 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
47fn 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.
58fn 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
94fn 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
104fn 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
121fn 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
134fn 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]
146fn 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]
204fn 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]
216fn 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]
241fn 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() {
604604 }
605605 }
606606 }
607 assert!(PreparedEdit::page(TREES, space, &widened, AUTHOR).is_err());
607 assert!(PreparedEdit::page(TREES, space, &widened, AUTHOR).is_ok());
608608 let mut relisted = page_in(TREES, space);
609609 let outline = body(&page)[0];
610610 for object in &mut relisted.objects {
tools/test_table_edit.py created+58
......@@ -0,0 +1,58 @@
1import html
2from pathlib import Path
3import re
4import runpy
5import shutil
6from tempfile import TemporaryDirectory
7import unittest
8import xml.etree.ElementTree as ET
9
10from native_xml import ns
11
12ROOT = Path(__file__).resolve().parent.parent
13FIXTURE = ROOT / 'corpus/table-edit'
14compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare']
15
16
17def 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
32class 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
57if __name__ == '__main__':
58 unittest.main()