| author | |
| committer | |
| log | bcb13ba046299393ccadffa70cc2c57963ffc3ce |
| tree | e3385fdebbc3ae00d47dc853bbdf7aa52062cc8e |
| parent | 23f0f23fc7064901d2942eb0d0ff5c4adea3afd6 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
OneNote 2010 was observed linking to a page, a paragraph and the section
through the COM API on a Rust-written page (corpus/link-edit/native-links,
authored by tools/native/page-link.ps1): each link is the same HYPERLINK
field code as an external one whose URL is the relative form
onenote:#Title&section-id={section file identity}&page-id={page
notebook-management identity}&end&base-path=<section path>, with
&object-id={paragraph identity}&n instead of &end for a paragraph and
neither title nor page for a section. The page model now carries that page
identity (Page::identity, from the metadata object, absent on damaged
pages), Section::identity exposes the file identity, and
onestore::page::link::internal_link builds the URL; the native URLs are
reproduced byte for byte from stored identities, and
corpus/link-edit/internal is the cold read of a Rust page linking to a
second page. The base path is the caller's, as OneNote writes its own.
The tag corpus is regenerated with a red-on-yellow definition so custom tag
colours and highlights have their native gate.
Assisted-by: claude-fable-5.161 files changed, 952 insertions(+), 34 deletions(-)
corpus/link-edit/README.md+22| ... | ... | @@ -15,3 +15,25 @@ embedded objects and runs with associated data stay refused. |
| 15 | 15 | `ONESTORE_LINK_EXPORT` set to a new absolute directory while running the test, |
| 16 | 16 | then cold-open it with `tools/native_runner.py OUTPUT COLD --expected-pages 1 |
| 17 | 17 | --collect-notebook`. |
| 18 | ||
| 19 | ## Internal links | |
| 20 | ||
| 21 | `native-links/` is OneNote 2010 adding links to a page, to a paragraph on it | |
| 22 | and to the section on the Rust-authored page above through the COM API | |
| 23 | (`tools/native/page-link.ps1`; `links.json` holds the `onenote:///…` URLs | |
| 24 | `GetHyperlinkToObject` returned, `update.xml` the submitted page). In | |
| 25 | `notebook/`, OneNote stored each as a `HYPERLINK` field code with the | |
| 26 | relative form `onenote:#Link%20target&section-id={section file identity} | |
| 27 | &page-id={page notebook-management identity}&end&base-path=<section path>`; | |
| 28 | a paragraph link ends with `&object-id={paragraph identity}&n` instead of | |
| 29 | `&end`, and a section link has neither title nor page. OneNote rewrote the | |
| 30 | target outline after linking, so its own paragraph link names an identity | |
| 31 | (`n` 28) the current outline no longer holds. | |
| 32 | ||
| 33 | `internal/candidate` is the writer's output for | |
| 34 | `a_page_links_to_another_page_and_its_paragraph` in | |
| 35 | `crates/onestore/tests/page_links.rs`: a section created in Rust with a | |
| 36 | second page, whose first page links to that page and to its first paragraph | |
| 37 | with URLs built by `onestore::page::link::internal_link`. `internal/cold` is | |
| 38 | its cold read with both links. Regenerate with | |
| 39 | `ONESTORE_INTERNAL_LINK_EXPORT` and cold-open with `--expected-pages 2`. |
corpus/link-edit/internal/candidate/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/link-edit/internal/candidate/Open Notebook.onetoc2 differ |
corpus/link-edit/internal/candidate/links.one created| Binary files /dev/null and b/corpus/link-edit/internal/candidate/links.one differ |
corpus/link-edit/internal/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-A4418B81 -ExpectedPages 2", "exit": 0, "stdout": "Read 1 sections and 2 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/link-edit/internal/cold/machine.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"name": "m6-a4418b81", "hostname": "ONE-M6-A4418B81"} |
corpus/link-edit/internal/cold/notebook/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/link-edit/internal/cold/notebook/Open Notebook.onetoc2 differ |
corpus/link-edit/internal/cold/notebook/links.one created| Binary files /dev/null and b/corpus/link-edit/internal/cold/notebook/links.one differ |
corpus/link-edit/internal/cold/read/environment.json created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | { | |
| 2 | "powershell": "5.1.14409.1005", | |
| 3 | "schema": "xs2010", | |
| 4 | "hostname": "ONE-M6-A4418B81", | |
| 5 | "cold": true, | |
| 6 | "onenote": "14.0.4763.1000" | |
| 7 | } |
corpus/link-edit/internal/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="{A475638B-F081-40E1-B0A7-056122CE68EA}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-12T22:13:46.000Z" color="#B49EDE"><one:Section name="links" ID="{D78A6D3C-7091-0DF6-2EFC-514D3F2E4C85}{1}{B0}" path="C:\one-tests\runs\capture\notebook\links.one" lastModifiedTime="2026-09-12T22:13:46.000Z" color="#8AA8E4"><one:Page ID="{8D0B3F30-F486-0365-1D6A-714CBE91E07B}{1}{B0}" name="Linking page HYPERLINK &quot;onenote:#Link%20target&amp;section-id={A45E149D-BEE4-4BB2-AF69-072EF644999A}&amp;page-id={1E1AB5A6-E512-42BE-A763-78914DE104F9}&amp;end&amp;base-path=C:\one-tests\runs\capture\notebook\links.one&quot;Link target" dateTime="2026-09-12T22:12:09.000Z" lastModifiedTime="2026-09-12T22:13:46.000Z" pageLevel="1"/><one:Page ID="{109B4059-8CAB-09BB-0559-34EA53917DD2}{1}{B0}" name="Link target" dateTime="2026-09-12T22:12:09.000Z" lastModifiedTime="2026-09-12T22:13:46.000Z" pageLevel="1"/></one:Section></one:Notebook> |
corpus/link-edit/internal/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="{109B4059-8CAB-09BB-0559-34EA53917DD2}{1}{B0}" name="Link target" dateTime="2026-09-12T22:12:09.000Z" lastModifiedTime="2026-09-12T22:13:46.000Z" pageLevel="1"><one:QuickStyleDef index="0" name="PageTitle" font="Calibri" fontSize="17.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title showDate="false" showTime="false" lang="en-US"><one:OE author="Author" lastModifiedBy="Author" creationTime="2026-09-12T22:12:09.000Z" lastModifiedTime="2026-09-12T22:12:09.000Z" objectID="{109B4059-8CAB-09BB-0559-34EA53917DD2}{15}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Link target]]></one:T></one:OE></one:Title></one:Page> |
corpus/link-edit/internal/cold/read/page-001.xml created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{8D0B3F30-F486-0365-1D6A-714CBE91E07B}{1}{B0}" name="Linking page HYPERLINK &quot;onenote:#Link%20target&amp;section-id={A45E149D-BEE4-4BB2-AF69-072EF644999A}&amp;page-id={1E1AB5A6-E512-42BE-A763-78914DE104F9}&amp;end&amp;base-path=C:\one-tests\runs\capture\notebook\links.one&quot;Link target" dateTime="2026-09-12T22:12:09.000Z" lastModifiedTime="2026-09-12T22:13: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-12T22:12:09.000Z" objectID="{D78A6D3C-7091-0DF6-2EFC-514D3F2E4C85}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="172.3725128173828" height="13.4277172088623"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T22:12:09.000Z" lastModifiedTime="2026-09-12T22:12:09.000Z" objectID="{D78A6D3C-7091-0DF6-2EFC-514D3F2E4C85}{24}{B0}" alignment="left"><one:T><![CDATA[Linking page <a | |
| 3 | href="onenote:#Link%20target&amp;section-id={A45E149D-BEE4-4BB2-AF69-072EF644999A}&amp;page-id={1E1AB5A6-E512-42BE-A763-78914DE104F9}&amp;end&amp;base-path=C:\one-tests\runs\capture\notebook\links.one">Link target</a>]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/link-edit/internal/cold/read/payloads.json created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | [ | |
| 2 | ||
| 3 | ] | |
| \ No newline at end of file |
corpus/link-edit/internal/cold/run.json created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | { | |
| 2 | "notebook": "/private/tmp/m4/ilink", | |
| 3 | "expected_pages": 2, | |
| 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/link-edit/internal/cold/scripts/cold.ps1 created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') | |
| 2 | Set-StrictMode -Version Latest | |
| 3 | $ErrorActionPreference = 'Stop' | |
| 4 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 5 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 6 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 7 | } | |
| 8 | if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } | |
| 9 | $key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' | |
| 10 | if ($CloneHost) { | |
| 11 | if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { | |
| 12 | throw 'The disposable clone hostname does not match this machine.' | |
| 13 | } | |
| 14 | New-Item "$key\Options\Paths" -Force | Out-Null | |
| 15 | New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null | |
| 16 | } elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or | |
| 17 | -not (Test-Path 'C:\one-tests\profile-original-cache')) { | |
| 18 | throw 'Park the personal OneNote profile before resetting the test cache.' | |
| 19 | } | |
| 20 | $cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' | |
| 21 | $parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) | |
| 22 | if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } | |
| 23 | New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null | |
| 24 | if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } | |
| 25 | if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } | |
| 26 | New-Item "$key\OpenNotebooks" | Out-Null | |
| 27 | New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null |
corpus/link-edit/internal/cold/scripts/read.ps1 created+142| ... | ... | @@ -0,0 +1,142 @@ |
| 1 | param( | |
| 2 | [Parameter(Mandatory=$true)][string]$Root, | |
| 3 | [int]$ExpectedPages = -1, | |
| 4 | [switch]$UseCurrentCache, | |
| 5 | [switch]$Pdf, | |
| 6 | [switch]$KeepOpen, | |
| 7 | [string]$CloneHost = '' | |
| 8 | ) | |
| 9 | Set-StrictMode -Version Latest | |
| 10 | $ErrorActionPreference = 'Stop' | |
| 11 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 12 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 13 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 14 | } | |
| 15 | $notebook = Join-Path $root 'notebook' | |
| 16 | $output = Join-Path $root 'read' | |
| 17 | if (Test-Path $output) { throw 'Choose a new read destination.' } | |
| 18 | if ($UseCurrentCache) { | |
| 19 | if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { | |
| 20 | throw 'Park the personal OneNote profile before reading test notebooks.' | |
| 21 | } | |
| 22 | } else { | |
| 23 | & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost | |
| 24 | } | |
| 25 | New-Item -ItemType Directory -Path $output | Out-Null | |
| 26 | $app = New-Object -ComObject OneNote.Application | |
| 27 | $notebookId = '' | |
| 28 | $failure = $null | |
| 29 | try { | |
| 30 | $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) | |
| 31 | $process = Get-Process ONENOTE | |
| 32 | @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; | |
| 33 | powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | | |
| 34 | ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 | |
| 35 | $sections = @() | |
| 36 | foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { | |
| 37 | $id = '' | |
| 38 | $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) | |
| 39 | $sections += $id | |
| 40 | } | |
| 41 | $deadline = [DateTime]::UtcNow.AddSeconds(300) | |
| 42 | $previous = '' | |
| 43 | $lastChange = '' | |
| 44 | $stableSince = [DateTime]::UtcNow | |
| 45 | $settled = $false | |
| 46 | do { | |
| 47 | $pages = @{} | |
| 48 | foreach ($section in $sections) { | |
| 49 | $hierarchy = '' | |
| 50 | $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) | |
| 51 | [xml]$xml = $hierarchy | |
| 52 | foreach ($node in $xml.SelectNodes('//*[@path]')) { | |
| 53 | if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { | |
| 54 | throw 'OneNote opened a section outside the copied notebook.' | |
| 55 | } | |
| 56 | } | |
| 57 | foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { | |
| 58 | $id = $node.GetAttribute('ID') | |
| 59 | $content = '' | |
| 60 | $app.GetPageContent($id, [ref]$content, 1, 1) | |
| 61 | $pages[$id] = $content | |
| 62 | } | |
| 63 | } | |
| 64 | $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) | |
| 65 | if ($signature -ne $previous) { | |
| 66 | $lastChange = $previous | |
| 67 | $previous = $signature | |
| 68 | $stableSince = [DateTime]::UtcNow | |
| 69 | } | |
| 70 | if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or | |
| 71 | ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and | |
| 72 | ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } | |
| 73 | Start-Sleep -Milliseconds 250 | |
| 74 | } while ([DateTime]::UtcNow -lt $deadline) | |
| 75 | if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { | |
| 76 | [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) | |
| 77 | $index = 0 | |
| 78 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 79 | [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 80 | $index++ | |
| 81 | } | |
| 82 | $hierarchy = '' | |
| 83 | $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) | |
| 84 | [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) | |
| 85 | throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." | |
| 86 | } | |
| 87 | $index = 0 | |
| 88 | $payloads = @() | |
| 89 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 90 | [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 91 | if ($Pdf) { | |
| 92 | $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) | |
| 93 | $app.NavigateTo($id, '', $false) | |
| 94 | $app.Publish($id, $pdfPath, 3, '') | |
| 95 | if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } | |
| 96 | } | |
| 97 | [xml]$page = $pages[$id] | |
| 98 | foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { | |
| 99 | $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) | |
| 100 | $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() | |
| 101 | [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) | |
| 102 | $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); | |
| 103 | kind = $file.LocalName; name = $file.GetAttribute('preferredName'); | |
| 104 | sha256 = $hash; bytes = $bytes.Length } | |
| 105 | } | |
| 106 | $index++ | |
| 107 | } | |
| 108 | [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) | |
| 109 | $all = '' | |
| 110 | $app.GetHierarchy($notebookId, 4, [ref]$all, 1) | |
| 111 | [xml]$finalTree = $all | |
| 112 | $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) | |
| 113 | if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { | |
| 114 | throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' | |
| 115 | } | |
| 116 | [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) | |
| 117 | Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." | |
| 118 | } catch { | |
| 119 | $failure = $_ | |
| 120 | [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 121 | throw | |
| 122 | } finally { | |
| 123 | try { | |
| 124 | try { | |
| 125 | if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } | |
| 126 | if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { | |
| 127 | $app.CloseNotebook($notebookId, $false) | |
| 128 | } | |
| 129 | } catch { | |
| 130 | if ($null -eq $failure) { throw } | |
| 131 | [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 132 | } | |
| 133 | } finally { | |
| 134 | [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) | |
| 135 | } | |
| 136 | $app = $null | |
| 137 | [GC]::Collect() | |
| 138 | [GC]::WaitForPendingFinalizers() | |
| 139 | if (-not $UseCurrentCache -and -not $CloneHost) { | |
| 140 | Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 | |
| 141 | } | |
| 142 | } |
corpus/link-edit/internal/cold/source.json created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | [ | |
| 2 | { | |
| 3 | "path": "Open Notebook.onetoc2", | |
| 4 | "bytes": 1604, | |
| 5 | "sha256": "fdee47831417ef5d21624890fe8d9f3d73a4ded56cacbbe2ebb978788786681b", | |
| 6 | "mtime_ns": 1789251129798178239 | |
| 7 | }, | |
| 8 | { | |
| 9 | "path": "links.one", | |
| 10 | "bytes": 9200, | |
| 11 | "sha256": "16b69f8178ce8a23e37ca844b89ae736f4e2c57827205fcfc1e51501b2865d17", | |
| 12 | "mtime_ns": 1789251129797893821 | |
| 13 | } | |
| 14 | ] |
corpus/link-edit/internal/cold/teardown.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"absent": true} |
corpus/link-edit/native-links/before.xml created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{920F855C-A13E-02F6-000A-0CE8D35FEB66}{1}{B0}" name="Read about Rust HYPERLINK &quot;https://example.invalid/rust&quot;the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-12T22:04:34.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="Author" lastModifiedBy="Author" lastModifiedTime="2026-09-12T09:49:16.000Z" objectID="{330243D5-C929-0DCB-16A1-2C882BBC629D}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="198.3904571533203" height="13.4277172088623"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-12T09:49:16.000Z" objectID="{330243D5-C929-0DCB-16A1-2C882BBC629D}{24}{B0}" alignment="left"><one:T><![CDATA[Read about Rust <a | |
| 3 | href="https://example.invalid/rust">the Rust site</a>]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/link-edit/native-links/commands.jsonl created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 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\\author.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-66B8F3A0", "exit": 0, "stdout": "", "stderr": "", "error": null} | |
| 3 | {"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-66B8F3A0 -ExpectedPages 2 -UseCurrentCache", "exit": 0, "stdout": "Read 1 sections and 2 pages.\r\n", "stderr": "", "error": null} | |
| 4 | {"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/link-edit/native-links/links.json created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | { | |
| 2 | "paragraph": "onenote:///C:\\one-tests\\runs\\capture\\notebook\\links.one#Link%20target\u0026section-id={F8C0DD10-38DD-49C1-B6E4-B882569883D1}\u0026page-id={566651E4-0067-42F8-883E-206560B4FCFA}\u0026object-id={1A505D95-B393-0320-0F64-0970FA28243C}\u002628", | |
| 3 | "section": "onenote:///C:\\one-tests\\runs\\capture\\notebook\\links.one#section-id={F8C0DD10-38DD-49C1-B6E4-B882569883D1}\u0026end", | |
| 4 | "target_paragraph": "{1DAB917D-E947-4865-BCEA-2612693D9939}{40}{B0}", | |
| 5 | "page": "onenote:///C:\\one-tests\\runs\\capture\\notebook\\links.one#Link%20target\u0026section-id={F8C0DD10-38DD-49C1-B6E4-B882569883D1}\u0026page-id={566651E4-0067-42F8-883E-206560B4FCFA}\u0026end", | |
| 6 | "target": "{38458021-4A30-4602-B127-7D34E7287AC7}{1}{B0}" | |
| 7 | } |
corpus/link-edit/native-links/machine.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"name": "m6-66b8f3a0", "hostname": "ONE-M6-66B8F3A0"} |
corpus/link-edit/native-links/notebook/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/link-edit/native-links/notebook/Open Notebook.onetoc2 differ |
corpus/link-edit/native-links/notebook/links.one created| Binary files /dev/null and b/corpus/link-edit/native-links/notebook/links.one differ |
corpus/link-edit/native-links/read/environment.json created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | { | |
| 2 | "powershell": "5.1.14409.1005", | |
| 3 | "schema": "xs2010", | |
| 4 | "hostname": "ONE-M6-66B8F3A0", | |
| 5 | "cold": false, | |
| 6 | "onenote": "14.0.4763.1000" | |
| 7 | } |
corpus/link-edit/native-links/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="{FD4B73ED-1D7D-4C62-8B6F-60B7DA2B502C}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-12T22:06:07.000Z" color="#B49EDE"><one:Section name="links" ID="{C588D77C-1E62-0A6F-2BBA-369AB0BA6184}{1}{B0}" path="C:\one-tests\runs\capture\notebook\links.one" lastModifiedTime="2026-09-12T22:06:07.000Z" color="#8AA8E4"><one:Page ID="{648511F5-7675-0552-3D11-16FA4859E87F}{1}{B0}" name="Read about Rust the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-12T22:06:07.000Z" pageLevel="1"/><one:Page ID="{CECF1488-9D7B-41A6-8C3C-67267C2E79DE}{1}{B0}" name="Link target" dateTime="2026-09-12T22:04:37.000Z" lastModifiedTime="2026-09-12T22:06:07.000Z" pageLevel="1"/></one:Section></one:Notebook> |
corpus/link-edit/native-links/read/page-000.xml created+6| ... | ... | @@ -0,0 +1,6 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{648511F5-7675-0552-3D11-16FA4859E87F}{1}{B0}" name="Read about Rust the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-12T22:06:07.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="Author" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-12T22:04:42.000Z" objectID="{C588D77C-1E62-0A6F-2BBA-369AB0BA6184}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="198.3904571533203" height="53.71086883544922"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-12T22:04:42.000Z" objectID="{C588D77C-1E62-0A6F-2BBA-369AB0BA6184}{24}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Read about Rust <a | |
| 3 | href="https://example.invalid/rust">the Rust site</a>]]></one:T></one:OE><one:OE creationTime="2026-09-12T22:04:42.000Z" lastModifiedTime="2026-09-12T22:04:42.000Z" objectID="{8B99BDC7-61D8-45CE-BBEC-39FFF44C8422}{25}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[<a | |
| 4 | href="onenote:#Link%20target&amp;section-id={F8C0DD10-38DD-49C1-B6E4-B882569883D1}&amp;page-id={566651E4-0067-42F8-883E-206560B4FCFA}&amp;end&amp;base-path=C:\one-tests\runs\capture\notebook\links.one">Page link</a>]]></one:T></one:OE><one:OE creationTime="2026-09-12T22:04:42.000Z" lastModifiedTime="2026-09-12T22:04:42.000Z" objectID="{8B99BDC7-61D8-45CE-BBEC-39FFF44C8422}{30}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[<a | |
| 5 | href="onenote:#Link%20target&amp;section-id={F8C0DD10-38DD-49C1-B6E4-B882569883D1}&amp;page-id={566651E4-0067-42F8-883E-206560B4FCFA}&amp;object-id={1A505D95-B393-0320-0F64-0970FA28243C}&amp;28&amp;base-path=C:\one-tests\runs\capture\notebook\links.one">Paragraph link</a>]]></one:T></one:OE><one:OE creationTime="2026-09-12T22:04:42.000Z" lastModifiedTime="2026-09-12T22:04:42.000Z" objectID="{8B99BDC7-61D8-45CE-BBEC-39FFF44C8422}{35}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[<a | |
| 6 | href="onenote:#section-id={F8C0DD10-38DD-49C1-B6E4-B882569883D1}&amp;end&amp;base-path=C:\one-tests\runs\capture\notebook\links.one">Section link</a>]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/link-edit/native-links/read/page-001.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="{CECF1488-9D7B-41A6-8C3C-67267C2E79DE}{1}{B0}" name="Link target" dateTime="2026-09-12T22:04:37.000Z" lastModifiedTime="2026-09-12T22:06:07.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:Title lang="en-US"><one:OE author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" creationTime="2026-09-12T22:04:38.000Z" lastModifiedTime="2026-09-12T22:04:38.000Z" objectID="{EB2105D4-3E0C-4FC1-81F1-3C00F23B9A20}{33}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Link target]]></one:T></one:OE></one:Title><one:Outline author="virtual" authorInitials="V" lastModifiedBy="virtual" lastModifiedByInitials="V" lastModifiedTime="2026-09-12T22:04:38.000Z" objectID="{EB2105D4-3E0C-4FC1-81F1-3C00F23B9A20}{39}{B0}"><one:Position x="36.0" y="86.0" z="0"/><one:Size width="117.0292816162109" height="13.42771339416504"/><one:OEChildren><one:OE creationTime="2026-09-12T22:04:38.000Z" lastModifiedTime="2026-09-12T22:04:38.000Z" objectID="{EB2105D4-3E0C-4FC1-81F1-3C00F23B9A20}{40}{B0}" alignment="left" quickStyleIndex="0"><one:T><![CDATA[Target paragraph]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/link-edit/native-links/read/payloads.json created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | [ | |
| 2 | ||
| 3 | ] | |
| \ No newline at end of file |
corpus/link-edit/native-links/run.json created+19| ... | ... | @@ -0,0 +1,19 @@ |
| 1 | { | |
| 2 | "notebook": "/Users/clo/dev/one/corpus/link-edit/candidate", | |
| 3 | "expected_pages": 2, | |
| 4 | "author": "tools/native/page-link.ps1", | |
| 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 | "author.ps1": "a67883aa9e5ffd80b9709d8558f37184cf05d79b28b35ce0db3b2f8a16b57225", | |
| 16 | "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", | |
| 17 | "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" | |
| 18 | } | |
| 19 | } |
corpus/link-edit/native-links/scripts/author.ps1 created+63| ... | ... | @@ -0,0 +1,63 @@ |
| 1 | param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) | |
| 2 | Set-StrictMode -Version Latest | |
| 3 | $ErrorActionPreference = 'Stop' | |
| 4 | & "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost | |
| 5 | $namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' | |
| 6 | $app = New-Object -ComObject OneNote.Application | |
| 7 | $notebookId = '' | |
| 8 | try { | |
| 9 | $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) | |
| 10 | $sectionId = '' | |
| 11 | $app.OpenHierarchy('links.one', $notebookId, [ref]$sectionId, 0) | |
| 12 | $deadline = [DateTime]::UtcNow.AddSeconds(30) | |
| 13 | $page = $null | |
| 14 | $pages = @() | |
| 15 | do { | |
| 16 | $hierarchy = '' | |
| 17 | $app.GetHierarchy($sectionId, 4, [ref]$hierarchy, 1) | |
| 18 | [xml]$tree = $hierarchy | |
| 19 | $pages = @($tree.SelectNodes('//*[local-name()="Page"]')) | |
| 20 | if ($pages.Count -eq 1) { | |
| 21 | $content = '' | |
| 22 | $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1) | |
| 23 | [xml]$page = $content | |
| 24 | if (@($page.SelectNodes('//*[local-name()="OE"]')).Count -ge 1) { break } | |
| 25 | } | |
| 26 | Start-Sleep -Milliseconds 250 | |
| 27 | } while ([DateTime]::UtcNow -lt $deadline) | |
| 28 | if ($pages.Count -ne 1) { throw 'Expected one page in the section.' } | |
| 29 | [IO.File]::WriteAllText((Join-Path $Root 'before.xml'), $content, [Text.Encoding]::UTF8) | |
| 30 | $targetId = '' | |
| 31 | $app.CreateNewPage($sectionId, [ref]$targetId, 0) | |
| 32 | $target = '<one:Page xmlns:one="' + $namespace + '" ID="' + $targetId + '"><one:Title><one:OE><one:T><![CDATA[Link target]]></one:T></one:OE></one:Title><one:Outline><one:Position x="36" y="86"/><one:Size width="300" height="30"/><one:OEChildren><one:OE><one:T><![CDATA[Target paragraph]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page>' | |
| 33 | $app.UpdatePageContent($target, [DateTime]::MinValue, 1, $true) | |
| 34 | $targetContent = '' | |
| 35 | $app.GetPageContent($targetId, [ref]$targetContent, 3, 1) | |
| 36 | [xml]$targetPage = $targetContent | |
| 37 | $targetParagraph = $targetPage.SelectSingleNode('//*[local-name()="Outline"]/*[local-name()="OEChildren"]/*[local-name()="OE"]').GetAttribute('objectID') | |
| 38 | $pageLink = '' | |
| 39 | $app.GetHyperlinkToObject($targetId, '', [ref]$pageLink) | |
| 40 | $paragraphLink = '' | |
| 41 | $app.GetHyperlinkToObject($targetId, $targetParagraph, [ref]$paragraphLink) | |
| 42 | $sectionLink = '' | |
| 43 | $app.GetHyperlinkToObject($sectionId, '', [ref]$sectionLink) | |
| 44 | @{ page = $pageLink; paragraph = $paragraphLink; section = $sectionLink; target = $targetId; target_paragraph = $targetParagraph } | ConvertTo-Json | Set-Content (Join-Path $Root 'links.json') -Encoding UTF8 | |
| 45 | $children = $page.SelectSingleNode('//*[local-name()="Outline"]/*[local-name()="OEChildren"]') | |
| 46 | foreach ($entry in @(@('Page link', $pageLink), @('Paragraph link', $paragraphLink), @('Section link', $sectionLink))) { | |
| 47 | $oe = $page.CreateElement('one', 'OE', $namespace) | |
| 48 | $t = $page.CreateElement('one', 'T', $namespace) | |
| 49 | [void]$t.AppendChild($page.CreateCDataSection('<a href="' + [Security.SecurityElement]::Escape($entry[1]) + '">' + $entry[0] + '</a>')) | |
| 50 | [void]$oe.AppendChild($t) | |
| 51 | [void]$children.AppendChild($oe) | |
| 52 | } | |
| 53 | [IO.File]::WriteAllText((Join-Path $Root 'update.xml'), $page.OuterXml, [Text.Encoding]::UTF8) | |
| 54 | $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false) | |
| 55 | $app.SyncHierarchy($notebookId) | |
| 56 | $app.CloseNotebook($notebookId, $false) | |
| 57 | $notebookId = '' | |
| 58 | } finally { | |
| 59 | if ($notebookId) { $app.CloseNotebook($notebookId, $false) } | |
| 60 | [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) | |
| 61 | [GC]::Collect() | |
| 62 | [GC]::WaitForPendingFinalizers() | |
| 63 | } |
corpus/link-edit/native-links/scripts/cold.ps1 created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') | |
| 2 | Set-StrictMode -Version Latest | |
| 3 | $ErrorActionPreference = 'Stop' | |
| 4 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 5 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 6 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 7 | } | |
| 8 | if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } | |
| 9 | $key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' | |
| 10 | if ($CloneHost) { | |
| 11 | if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { | |
| 12 | throw 'The disposable clone hostname does not match this machine.' | |
| 13 | } | |
| 14 | New-Item "$key\Options\Paths" -Force | Out-Null | |
| 15 | New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null | |
| 16 | } elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or | |
| 17 | -not (Test-Path 'C:\one-tests\profile-original-cache')) { | |
| 18 | throw 'Park the personal OneNote profile before resetting the test cache.' | |
| 19 | } | |
| 20 | $cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' | |
| 21 | $parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) | |
| 22 | if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } | |
| 23 | New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null | |
| 24 | if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } | |
| 25 | if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } | |
| 26 | New-Item "$key\OpenNotebooks" | Out-Null | |
| 27 | New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null |
corpus/link-edit/native-links/scripts/read.ps1 created+142| ... | ... | @@ -0,0 +1,142 @@ |
| 1 | param( | |
| 2 | [Parameter(Mandatory=$true)][string]$Root, | |
| 3 | [int]$ExpectedPages = -1, | |
| 4 | [switch]$UseCurrentCache, | |
| 5 | [switch]$Pdf, | |
| 6 | [switch]$KeepOpen, | |
| 7 | [string]$CloneHost = '' | |
| 8 | ) | |
| 9 | Set-StrictMode -Version Latest | |
| 10 | $ErrorActionPreference = 'Stop' | |
| 11 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 12 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 13 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 14 | } | |
| 15 | $notebook = Join-Path $root 'notebook' | |
| 16 | $output = Join-Path $root 'read' | |
| 17 | if (Test-Path $output) { throw 'Choose a new read destination.' } | |
| 18 | if ($UseCurrentCache) { | |
| 19 | if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { | |
| 20 | throw 'Park the personal OneNote profile before reading test notebooks.' | |
| 21 | } | |
| 22 | } else { | |
| 23 | & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost | |
| 24 | } | |
| 25 | New-Item -ItemType Directory -Path $output | Out-Null | |
| 26 | $app = New-Object -ComObject OneNote.Application | |
| 27 | $notebookId = '' | |
| 28 | $failure = $null | |
| 29 | try { | |
| 30 | $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) | |
| 31 | $process = Get-Process ONENOTE | |
| 32 | @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; | |
| 33 | powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | | |
| 34 | ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 | |
| 35 | $sections = @() | |
| 36 | foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { | |
| 37 | $id = '' | |
| 38 | $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) | |
| 39 | $sections += $id | |
| 40 | } | |
| 41 | $deadline = [DateTime]::UtcNow.AddSeconds(300) | |
| 42 | $previous = '' | |
| 43 | $lastChange = '' | |
| 44 | $stableSince = [DateTime]::UtcNow | |
| 45 | $settled = $false | |
| 46 | do { | |
| 47 | $pages = @{} | |
| 48 | foreach ($section in $sections) { | |
| 49 | $hierarchy = '' | |
| 50 | $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) | |
| 51 | [xml]$xml = $hierarchy | |
| 52 | foreach ($node in $xml.SelectNodes('//*[@path]')) { | |
| 53 | if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { | |
| 54 | throw 'OneNote opened a section outside the copied notebook.' | |
| 55 | } | |
| 56 | } | |
| 57 | foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { | |
| 58 | $id = $node.GetAttribute('ID') | |
| 59 | $content = '' | |
| 60 | $app.GetPageContent($id, [ref]$content, 1, 1) | |
| 61 | $pages[$id] = $content | |
| 62 | } | |
| 63 | } | |
| 64 | $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) | |
| 65 | if ($signature -ne $previous) { | |
| 66 | $lastChange = $previous | |
| 67 | $previous = $signature | |
| 68 | $stableSince = [DateTime]::UtcNow | |
| 69 | } | |
| 70 | if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or | |
| 71 | ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and | |
| 72 | ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } | |
| 73 | Start-Sleep -Milliseconds 250 | |
| 74 | } while ([DateTime]::UtcNow -lt $deadline) | |
| 75 | if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { | |
| 76 | [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) | |
| 77 | $index = 0 | |
| 78 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 79 | [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 80 | $index++ | |
| 81 | } | |
| 82 | $hierarchy = '' | |
| 83 | $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) | |
| 84 | [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) | |
| 85 | throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." | |
| 86 | } | |
| 87 | $index = 0 | |
| 88 | $payloads = @() | |
| 89 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 90 | [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 91 | if ($Pdf) { | |
| 92 | $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) | |
| 93 | $app.NavigateTo($id, '', $false) | |
| 94 | $app.Publish($id, $pdfPath, 3, '') | |
| 95 | if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } | |
| 96 | } | |
| 97 | [xml]$page = $pages[$id] | |
| 98 | foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { | |
| 99 | $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) | |
| 100 | $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() | |
| 101 | [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) | |
| 102 | $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); | |
| 103 | kind = $file.LocalName; name = $file.GetAttribute('preferredName'); | |
| 104 | sha256 = $hash; bytes = $bytes.Length } | |
| 105 | } | |
| 106 | $index++ | |
| 107 | } | |
| 108 | [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) | |
| 109 | $all = '' | |
| 110 | $app.GetHierarchy($notebookId, 4, [ref]$all, 1) | |
| 111 | [xml]$finalTree = $all | |
| 112 | $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) | |
| 113 | if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { | |
| 114 | throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' | |
| 115 | } | |
| 116 | [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) | |
| 117 | Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." | |
| 118 | } catch { | |
| 119 | $failure = $_ | |
| 120 | [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 121 | throw | |
| 122 | } finally { | |
| 123 | try { | |
| 124 | try { | |
| 125 | if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } | |
| 126 | if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { | |
| 127 | $app.CloseNotebook($notebookId, $false) | |
| 128 | } | |
| 129 | } catch { | |
| 130 | if ($null -eq $failure) { throw } | |
| 131 | [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 132 | } | |
| 133 | } finally { | |
| 134 | [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) | |
| 135 | } | |
| 136 | $app = $null | |
| 137 | [GC]::Collect() | |
| 138 | [GC]::WaitForPendingFinalizers() | |
| 139 | if (-not $UseCurrentCache -and -not $CloneHost) { | |
| 140 | Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 | |
| 141 | } | |
| 142 | } |
corpus/link-edit/native-links/source.json created+14| ... | ... | @@ -0,0 +1,14 @@ |
| 1 | [ | |
| 2 | { | |
| 3 | "path": "Open Notebook.onetoc2", | |
| 4 | "bytes": 1604, | |
| 5 | "sha256": "6731c4214437fe1cb6aa347b0004542b7f7e4260f47773bf9ec07c4e894de47c", | |
| 6 | "mtime_ns": 1789206821144296891 | |
| 7 | }, | |
| 8 | { | |
| 9 | "path": "links.one", | |
| 10 | "bytes": 6128, | |
| 11 | "sha256": "0cfb356ed374fe919f73fb73fdbe5d66e0baa484d4119f70074d9f869d6db78a", | |
| 12 | "mtime_ns": 1789206821116200533 | |
| 13 | } | |
| 14 | ] |
corpus/link-edit/native-links/teardown.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"absent": true} |
corpus/link-edit/native-links/update.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?><one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{920F855C-A13E-02F6-000A-0CE8D35FEB66}{1}{B0}" name="Read about Rust HYPERLINK &quot;https://example.invalid/rust&quot;the Rust site" dateTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-12T22:04:34.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic /></one:PageSize><one:RuleLines visible="false" /></one:PageSettings><one:Outline author="Author" lastModifiedBy="Author" lastModifiedTime="2026-09-12T09:49:16.000Z" objectID="{330243D5-C929-0DCB-16A1-2C882BBC629D}{23}{B0}"><one:Position x="36.0" y="36.0" z="0" /><one:Size width="198.3904571533203" height="13.4277172088623" /><one:Indents><one:Indent level="0" indent="0.0" /></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T09:49:16.000Z" lastModifiedTime="2026-09-12T09:49:16.000Z" objectID="{330243D5-C929-0DCB-16A1-2C882BBC629D}{24}{B0}" alignment="left"><one:T><![CDATA[Read about Rust <a | |
| 2 | href="https://example.invalid/rust">the Rust site</a>]]></one:T></one:OE><one:OE><one:T><![CDATA[<a href="onenote:///C:\one-tests\runs\capture\notebook\links.one#Link%20target&amp;section-id={F8C0DD10-38DD-49C1-B6E4-B882569883D1}&amp;page-id={566651E4-0067-42F8-883E-206560B4FCFA}&amp;end">Page link</a>]]></one:T></one:OE><one:OE><one:T><![CDATA[<a href="onenote:///C:\one-tests\runs\capture\notebook\links.one#Link%20target&amp;section-id={F8C0DD10-38DD-49C1-B6E4-B882569883D1}&amp;page-id={566651E4-0067-42F8-883E-206560B4FCFA}&amp;object-id={1A505D95-B393-0320-0F64-0970FA28243C}&amp;28">Paragraph link</a>]]></one:T></one:OE><one:OE><one:T><![CDATA[<a href="onenote:///C:\one-tests\runs\capture\notebook\links.one#section-id={F8C0DD10-38DD-49C1-B6E4-B882569883D1}&amp;end">Section link</a>]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | |
| \ No newline at end of file |
corpus/tag-edit/README.md+3-2| ... | ... | @@ -4,7 +4,7 @@ |
| 4 | 4 | `new_definitions_and_tags_publish_on_a_fresh_page` in |
| 5 | 5 | `crates/onestore/tests/page_tags.rs`: on a section created in Rust, two tag |
| 6 | 6 | definitions are added ("Rust task", type 0, symbol 3; "Important", type 1, |
| 7 | symbol 13), the first paragraph gains an open task, a second paragraph a | |
| 7 | symbol 13, red text on a yellow highlight), the first paragraph gains an open task, a second paragraph a | |
| 8 | 8 | completed task with creation and completion dates, a third paragraph both |
| 9 | 9 | tags, and a fourth paragraph none. Tags are property-set arrays on the text |
| 10 | 10 | object referencing definition objects (jcid 0x120043) that carry the model's |
| ... | ... | @@ -12,7 +12,8 @@ identities after squash; an element holds one tag per action type, as OneNote |
| 12 | 12 | requires. |
| 13 | 13 | |
| 14 | 14 | `cold/` is a fresh OneNote 2010 read: both `TagDef` entries appear with their |
| 15 | type, symbol and name, the tags read back with their completion state and | |
| 15 | type, symbol, name and colours (`fontColor="#FF0000"`, | |
| 16 | `highlightColor="#FFFF00"` for the second), the tags read back with their completion state and | |
| 16 | 17 | dates, and the untagged paragraph has none. `tools/test_tag_edit.py` checks |
| 17 | 18 | this without a VM. Regenerate with `ONESTORE_TAG_EXPORT` set to a new absolute |
| 18 | 19 | directory while running the test, then cold-open it with |
corpus/tag-edit/candidate/Open Notebook.onetoc2| Binary files a/corpus/tag-edit/candidate/Open Notebook.onetoc2 and b/corpus/tag-edit/candidate/Open Notebook.onetoc2 differ |
corpus/tag-edit/candidate/tags.one| Binary files a/corpus/tag-edit/candidate/tags.one and b/corpus/tag-edit/candidate/tags.one differ |
corpus/tag-edit/cold/commands.jsonl+1-1| ... | ... | @@ -1,3 +1,3 @@ |
| 1 | 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-CFD607C8 -ExpectedPages 1", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "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-6BD92C98 -ExpectedPages 1", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null} | |
| 3 | 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/tag-edit/cold/machine.json+1-1| ... | ... | @@ -1 +1 @@ |
| 1 | {"name": "m6-cfd607c8", "hostname": "ONE-M6-CFD607C8"} | |
| 1 | {"name": "m6-6bd92c98", "hostname": "ONE-M6-6BD92C98"} |
corpus/tag-edit/cold/notebook/Open Notebook.onetoc2| Binary files a/corpus/tag-edit/cold/notebook/Open Notebook.onetoc2 and b/corpus/tag-edit/cold/notebook/Open Notebook.onetoc2 differ |
corpus/tag-edit/cold/notebook/tags.one| Binary files a/corpus/tag-edit/cold/notebook/tags.one and b/corpus/tag-edit/cold/notebook/tags.one differ |
corpus/tag-edit/cold/read/environment.json+1-1| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | { |
| 2 | 2 | "powershell": "5.1.14409.1005", |
| 3 | 3 | "schema": "xs2010", |
| 4 | "hostname": "ONE-M6-CFD607C8", | |
| 4 | "hostname": "ONE-M6-6BD92C98", | |
| 5 | 5 | "cold": true, |
| 6 | 6 | "onenote": "14.0.4763.1000" |
| 7 | 7 | } |
corpus/tag-edit/cold/read/hierarchy.xml+1-1| ... | ... | @@ -1,2 +1,2 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{776770BA-9325-43FF-BB17-ADC70305D68D}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-12T09:33:49.000Z" color="#B49EDE"><one:Section name="tags" ID="{124F54B0-18DB-05C6-2862-7599822E55B2}{1}{B0}" path="C:\one-tests\runs\capture\notebook\tags.one" lastModifiedTime="2026-09-12T09:33:49.000Z" color="#8AA8E4"><one:Page ID="{8B9A6BBA-7DEE-0679-2C42-81467412EBEF}{1}{B0}" name="Open task" dateTime="2026-09-12T09:32:16.000Z" lastModifiedTime="2026-09-12T09:33:49.000Z" pageLevel="1"/></one:Section></one:Notebook> | |
| 2 | <one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{D0E58B8A-B51D-44EF-8DA1-CA3B30B9B685}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-12T22:04:15.000Z" color="#B49EDE"><one:Section name="tags" ID="{895156DB-06D9-07F3-3689-416780D80F8B}{1}{B0}" path="C:\one-tests\runs\capture\notebook\tags.one" lastModifiedTime="2026-09-12T22:04:15.000Z" color="#8AA8E4"><one:Page ID="{E94AB3B0-8E65-0A67-380D-75B50FFFFECD}{1}{B0}" name="Open task" dateTime="2026-09-12T22:02:47.000Z" lastModifiedTime="2026-09-12T22:04:15.000Z" pageLevel="1"/></one:Section></one:Notebook> |
corpus/tag-edit/cold/read/page-000.xml+1-1| ... | ... | @@ -1,2 +1,2 @@ |
| 1 | 1 | <?xml version="1.0"?> |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{8B9A6BBA-7DEE-0679-2C42-81467412EBEF}{1}{B0}" name="Open task" dateTime="2026-09-12T09:32:16.000Z" lastModifiedTime="2026-09-12T09:33:49.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Rust task"/><one:TagDef index="1" type="1" symbol="13" fontColor="automatic" highlightColor="none" name="Important"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="Author" lastModifiedBy="Author" lastModifiedTime="2026-09-12T09:32:16.000Z" objectID="{124F54B0-18DB-05C6-2862-7599822E55B2}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="158.2036895751953" height="53.71086883544922"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T09:32:16.000Z" lastModifiedTime="2026-09-12T09:32:16.000Z" objectID="{124F54B0-18DB-05C6-2862-7599822E55B2}{24}{B0}" alignment="left"><one:Tag index="0" completed="false" disabled="false" creationDate="2020-01-02T03:04:05.000Z"/><one:T><![CDATA[Open task]]></one:T></one:OE><one:OE author="Tag author" lastModifiedBy="Tag author" creationTime="2026-09-12T09:32:16.000Z" lastModifiedTime="2026-09-12T09:32:16.000Z" objectID="{13111015-B879-0D3E-10E3-0C769EF30D4A}{0}{B0}" alignment="left"><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[Completed task]]></one:T></one:OE><one:OE author="Tag author" lastModifiedBy="Tag author" creationTime="2026-09-12T09:32:16.000Z" lastModifiedTime="2026-09-12T09:32:16.000Z" objectID="{F2B0539D-F9DE-062B-07BC-8EFA0C693E00}{0}{B0}" alignment="left"><one:Tag index="0" completed="false" disabled="false" creationDate="2020-01-02T03:04:05.000Z"/><one:Tag index="1" completed="false" disabled="false" creationDate="2020-01-02T03:04:05.000Z"/><one:T><![CDATA[Important open task]]></one:T></one:OE><one:OE author="Tag author" lastModifiedBy="Tag author" creationTime="2026-09-12T09:32:16.000Z" lastModifiedTime="2026-09-12T09:32:16.000Z" objectID="{8F65AAD5-5CD7-0F4E-24EF-4FACE80A618B}{0}{B0}" alignment="left"><one:T><![CDATA[Untagged]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{E94AB3B0-8E65-0A67-380D-75B50FFFFECD}{1}{B0}" name="Open task" dateTime="2026-09-12T22:02:47.000Z" lastModifiedTime="2026-09-12T22:04:15.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Rust task"/><one:TagDef index="1" type="1" symbol="13" fontColor="#FF0000" highlightColor="#FFFF00" name="Important"/><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-12T22:02:47.000Z" objectID="{895156DB-06D9-07F3-3689-416780D80F8B}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="158.2036895751953" height="53.71086883544922"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-12T22:02:47.000Z" lastModifiedTime="2026-09-12T22:02:47.000Z" objectID="{895156DB-06D9-07F3-3689-416780D80F8B}{24}{B0}" alignment="left"><one:Tag index="0" completed="false" disabled="false" creationDate="2020-01-02T03:04:05.000Z"/><one:T><![CDATA[Open task]]></one:T></one:OE><one:OE author="Tag author" lastModifiedBy="Tag author" creationTime="2026-09-12T22:02:47.000Z" lastModifiedTime="2026-09-12T22:02:47.000Z" objectID="{5D7040B7-F4B6-0C4D-00DA-A8932314CF98}{0}{B0}" alignment="left"><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[Completed task]]></one:T></one:OE><one:OE author="Tag author" lastModifiedBy="Tag author" creationTime="2026-09-12T22:02:47.000Z" lastModifiedTime="2026-09-12T22:02:47.000Z" objectID="{66249A3A-3DE8-0181-0785-5664C59CE40C}{0}{B0}" alignment="left"><one:Tag index="0" completed="false" disabled="false" creationDate="2020-01-02T03:04:05.000Z"/><one:Tag index="1" completed="false" disabled="false" creationDate="2020-01-02T03:04:05.000Z"/><one:T><![CDATA[Important open task]]></one:T></one:OE><one:OE author="Tag author" lastModifiedBy="Tag author" creationTime="2026-09-12T22:02:47.000Z" lastModifiedTime="2026-09-12T22:02:47.000Z" objectID="{D58C9666-1D8D-03DD-2071-2D3AA71E2BE9}{0}{B0}" alignment="left"><one:T><![CDATA[Untagged]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/tag-edit/cold/source.json+5-5| ... | ... | @@ -2,13 +2,13 @@ |
| 2 | 2 | { |
| 3 | 3 | "path": "Open Notebook.onetoc2", |
| 4 | 4 | "bytes": 1596, |
| 5 | "sha256": "f3ce1472c9be35b1fb52683c5214e62850787e8e13bbd1918fed81fd19a48a65", | |
| 6 | "mtime_ns": 1789205536390985808 | |
| 5 | "sha256": "4f64a5de0f6a4a1bfc92ec83d50f6cfd986b0c72d803dd9096b277b294b1288f", | |
| 6 | "mtime_ns": 1789250567099455316 | |
| 7 | 7 | }, |
| 8 | 8 | { |
| 9 | 9 | "path": "tags.one", |
| 10 | "bytes": 8352, | |
| 11 | "sha256": "601a8998b691bab66830196b3b139be6ea58d6f29d5bc69b3efa9892e2fc1550", | |
| 12 | "mtime_ns": 1789205536390698932 | |
| 10 | "bytes": 8368, | |
| 11 | "sha256": "bb1715165c52bcc443a012178604d9d1c9b5d4d9fb9c18b6ecb806583db5305d", | |
| 12 | "mtime_ns": 1789250567099157565 | |
| 13 | 13 | } |
| 14 | 14 | ] |
crates/canvas/src/editor.rs+4| ... | ... | @@ -114,6 +114,7 @@ pub struct CanvasEditor { |
| 114 | 114 | #[derive(Default)] |
| 115 | 115 | struct PageHeader { |
| 116 | 116 | title: String, |
| 117 | identity: Option<[u8; 16]>, | |
| 117 | 118 | created: Option<u64>, |
| 118 | 119 | margin_origin: [f32; 2], |
| 119 | 120 | areas: Vec<page::TitleArea>, |
| ... | ... | @@ -699,6 +700,7 @@ impl CanvasEditor { |
| 699 | 700 | editor.objects = objects; |
| 700 | 701 | editor.header = PageHeader { |
| 701 | 702 | title: page.title, |
| 703 | identity: page.identity, | |
| 702 | 704 | created: page.created, |
| 703 | 705 | margin_origin: page.margin_origin, |
| 704 | 706 | areas, |
| ... | ... | @@ -767,6 +769,7 @@ impl CanvasEditor { |
| 767 | 769 | ); |
| 768 | 770 | Ok(Page { |
| 769 | 771 | title: self.header.title.clone(), |
| 772 | identity: self.header.identity, | |
| 770 | 773 | created: self |
| 771 | 774 | .date |
| 772 | 775 | .as_ref() |
| ... | ... | @@ -4384,6 +4387,7 @@ mod tests { |
| 4384 | 4387 | let mut editor = CanvasEditor::from_page( |
| 4385 | 4388 | Page { |
| 4386 | 4389 | title: "Header".into(), |
| 4390 | identity: None, | |
| 4387 | 4391 | created: None, |
| 4388 | 4392 | margin_origin: [36.0, 14.4], |
| 4389 | 4393 | definitions: BTreeMap::new(), |
crates/canvas/src/gpu/page.rs+7| ... | ... | @@ -550,6 +550,7 @@ mod tests { |
| 550 | 550 | let id = fields[0].id; |
| 551 | 551 | let source = fields[0].paragraphs.clone(); |
| 552 | 552 | let page = Page { |
| 553 | identity: None, | |
| 553 | 554 | created: Some(1), |
| 554 | 555 | title: "Header".into(), |
| 555 | 556 | margin_origin: [36.0, 14.4], |
| ... | ... | @@ -987,6 +988,7 @@ mod tests { |
| 987 | 988 | fields[0].min_width = Some(162.0); |
| 988 | 989 | let title_id = fields[0].id; |
| 989 | 990 | let page = Page { |
| 991 | identity: None, | |
| 990 | 992 | created: None, |
| 991 | 993 | title: "Header".into(), |
| 992 | 994 | margin_origin: [36.0, 14.4], |
| ... | ... | @@ -1060,6 +1062,7 @@ mod tests { |
| 1060 | 1062 | }) |
| 1061 | 1063 | .collect(); |
| 1062 | 1064 | let page = Page { |
| 1065 | identity: None, | |
| 1063 | 1066 | created: None, |
| 1064 | 1067 | title: String::new(), |
| 1065 | 1068 | margin_origin: [0.0; 2], |
| ... | ... | @@ -1138,6 +1141,7 @@ mod tests { |
| 1138 | 1141 | PageObject::Outline(tagged) |
| 1139 | 1142 | }; |
| 1140 | 1143 | let page = Page { |
| 1144 | identity: None, | |
| 1141 | 1145 | created: None, |
| 1142 | 1146 | title: String::new(), |
| 1143 | 1147 | margin_origin: [36.0, 14.0], |
| ... | ... | @@ -1208,6 +1212,7 @@ mod tests { |
| 1208 | 1212 | layout: Layout::default(), |
| 1209 | 1213 | }; |
| 1210 | 1214 | let page = Page { |
| 1215 | identity: None, | |
| 1211 | 1216 | created: None, |
| 1212 | 1217 | title: String::new(), |
| 1213 | 1218 | margin_origin: [0.0; 2], |
| ... | ... | @@ -1383,6 +1388,7 @@ mod tests { |
| 1383 | 1388 | .unwrap(); |
| 1384 | 1389 | } |
| 1385 | 1390 | let page = || Page { |
| 1391 | identity: None, | |
| 1386 | 1392 | created: None, |
| 1387 | 1393 | title: String::new(), |
| 1388 | 1394 | margin_origin: [0.0; 2], |
| ... | ... | @@ -1504,6 +1510,7 @@ mod tests { |
| 1504 | 1510 | .unwrap(); |
| 1505 | 1511 | } |
| 1506 | 1512 | let page = |width| Page { |
| 1513 | identity: None, | |
| 1507 | 1514 | created: None, |
| 1508 | 1515 | title: String::new(), |
| 1509 | 1516 | margin_origin: [0.0; 2], |
crates/notebook/README.md+3-2| ... | ... | @@ -14,7 +14,7 @@ diffs the supplied model against the page stored in the supplied local snapshot, |
| 14 | 14 | writes the difference into the working image and queues one |
| 15 | 15 | `Operation::Page(PageIntent { before, after, author })`; `before` is the page the |
| 16 | 16 | edit started from and is the precondition reconciliation checks. Text, styles, |
| 17 | paragraph formatting, hyperlinks, bullets, numbering, note tags, table rows, columns, cell shading, nested tables, pictures (insertion in paragraphs or on the page, position, size, description) and attachments, paragraph structure, | |
| 17 | paragraph formatting, hyperlinks (external and internal), bullets, numbering, note tags, table rows, columns, cell shading, nested tables, pictures (insertion in paragraphs or on the page, position, size, description) and attachments, paragraph structure, | |
| 18 | 18 | outline layout, insertions and deletions are all differences between `before` and |
| 19 | 19 | `after`; the library never sees editor operations. |
| 20 | 20 | |
| ... | ... | @@ -60,7 +60,8 @@ a replica stored under the cache directory, named by the section's document |
| 60 | 60 | identity so the same file reopens the same queue after a relaunch. A section |
| 61 | 61 | publishes in the background to the file itself under OneNote-compatible |
| 62 | 62 | exclusion. `pages()` lists page spaces and titles from the local image, |
| 63 | `page(space)` returns the model to edit, and `save(space, before, after, | |
| 63 | `page(space)` returns the model to edit (its `identity` and the section's | |
| 64 | `identity()` feed `onestore::page::link::internal_link`), and `save(space, before, after, | |
| 64 | 65 | author)` queues the edited model: `Save::Queued(id)` is durable locally, |
| 65 | 66 | `Save::Unchanged` means the model equals the stored page, and `Save::Stale` |
| 66 | 67 | means the stored page no longer matches `before` because the section changed |
crates/notebook/src/merge.rs+1| ... | ... | @@ -116,6 +116,7 @@ pub(crate) fn merge(base: &Page, ours: &Page, theirs: &Page) -> Option<Page> { |
| 116 | 116 | } |
| 117 | 117 | Some(Page { |
| 118 | 118 | title: theirs.title.clone(), |
| 119 | identity: theirs.identity, | |
| 119 | 120 | created: theirs.created, |
| 120 | 121 | margin_origin: theirs.margin_origin, |
| 121 | 122 | objects: result, |
crates/notebook/src/session.rs+6| ... | ... | @@ -209,6 +209,12 @@ impl Section { |
| 209 | 209 | &self.file |
| 210 | 210 | } |
| 211 | 211 | |
| 212 | /// The section file identity, which internal links name as `section-id` | |
| 213 | /// (`onestore::page::link::internal_link`). | |
| 214 | pub fn identity(&self) -> Result<[u8; 16]> { | |
| 215 | Ok(Store::parse(&self.replica.snapshot()?)?.header.file_id) | |
| 216 | } | |
| 217 | ||
| 212 | 218 | /// Page spaces and titles in section order, from the local working image. |
| 213 | 219 | pub fn pages(&self) -> Result<Vec<(ExGuid, String)>> { |
| 214 | 220 | let snapshot = self.replica.snapshot()?; |
crates/onestore/README.md+1-1| ... | ... | @@ -57,7 +57,7 @@ harness also accepts `--client-profile release`. |
| 57 | 57 | | `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules | |
| 58 | 58 | | `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | |
| 59 | 59 | | `OutlineEdit`, `PreparedEdit::outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content | |
| 60 | | `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks, bullets and numbering, note tags, table rows, columns, cell shading and indents, nested tables, inserted pictures (in paragraphs or on the page) and attachments, picture position, size and description, 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 (external and, via `page::link`, to pages, paragraphs and sections), bullets and numbering, note tags, table rows, columns, cell shading and indents, nested tables, inserted pictures (in paragraphs or on the page) and attachments, picture position, size and description, paragraph insertion/split/join/move/deletion, outline insertion/deletion/position/width and saved collapse state, lowered onto the typed writers with the model's identities | | |
| 61 | 61 | | `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically | |
| 62 | 62 | | `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter | |
| 63 | 63 | | `read_file` | Read a snapshot under whole-file exclusion | |
crates/onestore/src/page/link.rs created+77| ... | ... | @@ -0,0 +1,77 @@ |
| 1 | //! Links between pages, paragraphs and sections of the same notebook, in the form OneNote | |
| 2 | //! stores them: a `HYPERLINK` field code whose URL names the section by its file identity | |
| 3 | //! and the page by its notebook-management identity, with the section path as `base-path`. | |
| 4 | ||
| 5 | use crate::ExGuid; | |
| 6 | ||
| 7 | /// What an internal link opens. | |
| 8 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 9 | pub enum LinkTarget<'a> { | |
| 10 | Section, | |
| 11 | Page { | |
| 12 | identity: [u8; 16], | |
| 13 | title: &'a str, | |
| 14 | }, | |
| 15 | /// A paragraph or other object on a page, named by its stored identity. | |
| 16 | Object { | |
| 17 | identity: [u8; 16], | |
| 18 | title: &'a str, | |
| 19 | object: ExGuid, | |
| 20 | }, | |
| 21 | } | |
| 22 | ||
| 23 | /// The URL OneNote 2010 stores for a link into the section whose file identity is `section` | |
| 24 | /// and whose file is at `base_path` (the path as the linking client sees it): | |
| 25 | /// `onenote:#Title&section-id={…}&page-id={…}&end&base-path=…`. | |
| 26 | pub fn internal_link(section: [u8; 16], base_path: &str, target: LinkTarget<'_>) -> String { | |
| 27 | let guid = |bytes: [u8; 16]| { | |
| 28 | ExGuid { guid: bytes, n: 0 } | |
| 29 | .to_string() | |
| 30 | .split(',') | |
| 31 | .next() | |
| 32 | .unwrap() | |
| 33 | .to_owned() | |
| 34 | }; | |
| 35 | let mut url = String::from("onenote:#"); | |
| 36 | match target { | |
| 37 | LinkTarget::Section => {} | |
| 38 | LinkTarget::Page { title, .. } | LinkTarget::Object { title, .. } => { | |
| 39 | url.push_str(&encoded(title)); | |
| 40 | url.push('&'); | |
| 41 | } | |
| 42 | } | |
| 43 | url.push_str(&format!("section-id={}", guid(section))); | |
| 44 | match target { | |
| 45 | LinkTarget::Section => url.push_str("&end"), | |
| 46 | LinkTarget::Page { identity, .. } => { | |
| 47 | url.push_str(&format!("&page-id={}&end", guid(identity))); | |
| 48 | } | |
| 49 | LinkTarget::Object { | |
| 50 | identity, object, .. | |
| 51 | } => { | |
| 52 | url.push_str(&format!( | |
| 53 | "&page-id={}&object-id={}&{}", | |
| 54 | guid(identity), | |
| 55 | guid(object.guid), | |
| 56 | object.n | |
| 57 | )); | |
| 58 | } | |
| 59 | } | |
| 60 | url.push_str("&base-path="); | |
| 61 | url.push_str(base_path); | |
| 62 | url | |
| 63 | } | |
| 64 | ||
| 65 | /// Percent-encodes a page title the way OneNote does in a link fragment. | |
| 66 | fn encoded(title: &str) -> String { | |
| 67 | let mut out = String::new(); | |
| 68 | for byte in title.bytes() { | |
| 69 | match byte { | |
| 70 | b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { | |
| 71 | out.push(byte as char); | |
| 72 | } | |
| 73 | _ => out.push_str(&format!("%{byte:02X}")), | |
| 74 | } | |
| 75 | } | |
| 76 | out | |
| 77 | } |
crates/onestore/src/page/mod.rs+36-15| ... | ... | @@ -10,6 +10,7 @@ use std::{ |
| 10 | 10 | sync::Arc, |
| 11 | 11 | }; |
| 12 | 12 | |
| 13 | pub mod link; | |
| 13 | 14 | pub mod text; |
| 14 | 15 | pub(crate) mod write; |
| 15 | 16 | pub use text::Paragraph; |
| ... | ... | @@ -24,6 +25,8 @@ pub enum DateField { |
| 24 | 25 | #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] |
| 25 | 26 | pub struct Page { |
| 26 | 27 | pub title: String, |
| 28 | /// The page's notebook-management identity, which internal links name as `page-id`. | |
| 29 | pub identity: Option<[u8; 16]>, | |
| 27 | 30 | /// FILETIME ticks from the page's TopologyCreationTimeStamp. |
| 28 | 31 | pub created: Option<u64>, |
| 29 | 32 | pub margin_origin: [f32; 2], |
| ... | ... | @@ -343,14 +346,21 @@ impl Page { |
| 343 | 346 | else { |
| 344 | 347 | return Err(invalid("Canvas root is not a page")); |
| 345 | 348 | }; |
| 349 | let metadata = revision | |
| 350 | .roots | |
| 351 | .get(&2) | |
| 352 | .and_then(|id| revision.nodes.get(id)) | |
| 353 | .filter(|node| matches!(node.kind, Kind::Metadata { .. })) | |
| 354 | .and_then(|node| node.extra.first()); | |
| 346 | 355 | let mut page = Self { |
| 347 | 356 | title: page_title(revision, id).unwrap_or_default().to_owned(), |
| 348 | created: revision | |
| 349 | .roots | |
| 350 | .get(&2) | |
| 351 | .and_then(|id| revision.nodes.get(id)) | |
| 352 | .filter(|node| matches!(node.kind, Kind::Metadata { .. })) | |
| 353 | .and_then(|node| node.extra.first()) | |
| 357 | identity: metadata | |
| 358 | .and_then(|fields| fields.iter().find(|field| field.id == 0x1c001c30)) | |
| 359 | .and_then(|field| match field.value { | |
| 360 | FieldValue::Bytes(bytes) => bytes.try_into().ok(), | |
| 361 | _ => None, | |
| 362 | }), | |
| 363 | created: metadata | |
| 354 | 364 | .and_then(|fields| fields.iter().find(|field| field.id == 0x18001c65)) |
| 355 | 365 | .map(|field| { |
| 356 | 366 | let FieldValue::Bytes(bytes) = field.value else { |
| ... | ... | @@ -847,7 +857,7 @@ impl PageParagraph { |
| 847 | 857 | #[cfg(test)] |
| 848 | 858 | mod tests { |
| 849 | 859 | use super::*; |
| 850 | use crate::document::{Element, TextRun}; | |
| 860 | use crate::document::{Element, Field, FieldValue, TextRun}; | |
| 851 | 861 | |
| 852 | 862 | fn id(n: u32) -> ExGuid { |
| 853 | 863 | ExGuid { |
| ... | ... | @@ -856,6 +866,8 @@ mod tests { |
| 856 | 866 | } |
| 857 | 867 | } |
| 858 | 868 | |
| 869 | const IDENTITY: [u8; 16] = [7; 16]; | |
| 870 | ||
| 859 | 871 | fn element(kind: Kind<'_>) -> Element<'_> { |
| 860 | 872 | Element { |
| 861 | 873 | jcid: 0, |
| ... | ... | @@ -918,10 +930,14 @@ mod tests { |
| 918 | 930 | name: Some("Body".into()), |
| 919 | 931 | }); |
| 920 | 932 | style.format.font_size = Some(12.0); |
| 921 | let metadata = element(Kind::Metadata { | |
| 933 | let mut metadata = element(Kind::Metadata { | |
| 922 | 934 | title: Some("Page title".into()), |
| 923 | 935 | level: None, |
| 924 | 936 | }); |
| 937 | metadata.extra = vec![vec![Field { | |
| 938 | id: 0x1c001c30, | |
| 939 | value: FieldValue::Bytes(&IDENTITY), | |
| 940 | }]]; | |
| 925 | 941 | Revision { |
| 926 | 942 | roots: BTreeMap::from([(2, id(6))]), |
| 927 | 943 | nodes: BTreeMap::from([ |
| ... | ... | @@ -1183,14 +1199,19 @@ mod tests { |
| 1183 | 1199 | |
| 1184 | 1200 | #[test] |
| 1185 | 1201 | fn creation_time_comes_from_page_metadata_and_date_role_from_the_title_child() { |
| 1186 | use crate::document::{Field, FieldValue}; | |
| 1187 | 1202 | let mut source = revision(); |
| 1188 | 1203 | source.nodes.get_mut(&id(1)).unwrap().created = Some(7); |
| 1189 | 1204 | let bytes = 134_333_468_649_123_456_u64.to_le_bytes(); |
| 1190 | source.nodes.get_mut(&id(6)).unwrap().extra = vec![vec![Field { | |
| 1191 | id: 0x18001c65, | |
| 1192 | value: FieldValue::Bytes(&bytes), | |
| 1193 | }]]; | |
| 1205 | source.nodes.get_mut(&id(6)).unwrap().extra = vec![vec![ | |
| 1206 | Field { | |
| 1207 | id: 0x18001c65, | |
| 1208 | value: FieldValue::Bytes(&bytes), | |
| 1209 | }, | |
| 1210 | Field { | |
| 1211 | id: 0x1c001c30, | |
| 1212 | value: FieldValue::Bytes(&IDENTITY), | |
| 1213 | }, | |
| 1214 | ]]; | |
| 1194 | 1215 | source.nodes.get_mut(&id(1)).unwrap().children.clear(); |
| 1195 | 1216 | source.nodes.get_mut(&id(1)).unwrap().structure.push(id(7)); |
| 1196 | 1217 | let mut title = element(Kind::Title); |
| ... | ... | @@ -1217,12 +1238,12 @@ mod tests { |
| 1217 | 1238 | source.nodes.get_mut(&id(6)).unwrap().extra[0][0].value = FieldValue::Bytes(&bytes[..7]); |
| 1218 | 1239 | assert!(Page::from_revision(&source, id(1)).is_err()); |
| 1219 | 1240 | source.nodes.get_mut(&id(6)).unwrap().extra.clear(); |
| 1220 | assert_eq!(Page::from_revision(&source, id(1)).unwrap().created, None); | |
| 1241 | let bare = Page::from_revision(&source, id(1)).unwrap(); | |
| 1242 | assert_eq!((bare.created, bare.identity), (None, None)); | |
| 1221 | 1243 | } |
| 1222 | 1244 | |
| 1223 | 1245 | #[test] |
| 1224 | 1246 | fn title_role_and_minimum_width_come_from_root_properties() { |
| 1225 | use crate::document::{Field, FieldValue}; | |
| 1226 | 1247 | let mut source = revision(); |
| 1227 | 1248 | source.nodes.get_mut(&id(1)).unwrap().children.clear(); |
| 1228 | 1249 | source.nodes.get_mut(&id(1)).unwrap().structure.push(id(7)); |
crates/onestore/tests/page_links.rs+149-1| ... | ... | @@ -1,11 +1,18 @@ |
| 1 | 1 | use onestore::{ |
| 2 | 2 | ExGuid, PreparedEdit, RevisionIndex, Store, |
| 3 | 3 | document::Document, |
| 4 | page::{Page, PageObject, PageParagraph, Paragraph, text::Edit}, | |
| 4 | page::{ | |
| 5 | Page, PageObject, PageParagraph, Paragraph, | |
| 6 | link::{LinkTarget, internal_link}, | |
| 7 | text::Edit, | |
| 8 | }, | |
| 5 | 9 | }; |
| 6 | 10 | |
| 7 | 11 | const OUTLINES: &[u8] = |
| 8 | 12 | include_bytes!("../../../corpus/outline-edit/before/notebook/synthetic.one"); |
| 13 | const NATIVE_LINKS: &[u8] = | |
| 14 | include_bytes!("../../../corpus/link-edit/native-links/notebook/links.one"); | |
| 15 | const NATIVE_BASE_PATH: &str = r"C:\one-tests\runs\capture\notebook\links.one"; | |
| 9 | 16 | const AUTHOR: &str = "Link author"; |
| 10 | 17 | const CODE: &str = "\u{fddf}HYPERLINK \"https://example.invalid/rust\""; |
| 11 | 18 | |
| ... | ... | @@ -191,3 +198,144 @@ fn a_link_is_added_to_a_fresh_page_and_reads_back() { |
| 191 | 198 | .unwrap(); |
| 192 | 199 | } |
| 193 | 200 | } |
| 201 | ||
| 202 | fn field_codes(page: &mut Page) -> Vec<String> { | |
| 203 | body_paragraphs(page) | |
| 204 | .iter() | |
| 205 | .filter_map(|p| p.text()) | |
| 206 | .filter_map(|t| { | |
| 207 | let text = t.text.text(); | |
| 208 | let start = text.find("\u{fddf}HYPERLINK \"")? + "\u{fddf}HYPERLINK \"".len(); | |
| 209 | let end = start + text[start..].find('"')?; | |
| 210 | Some(text[start..end].to_owned()) | |
| 211 | }) | |
| 212 | .collect() | |
| 213 | } | |
| 214 | ||
| 215 | /// The URLs OneNote 2010 wrote for links to a page, a paragraph and the section | |
| 216 | /// (`corpus/link-edit/native-links`) are what `internal_link` produces from the stored | |
| 217 | /// identities: the section file identity, the target page's notebook-management identity | |
| 218 | /// and the paragraph's stored identity. | |
| 219 | #[test] | |
| 220 | fn internal_links_match_what_onenote_stores() { | |
| 221 | let section = Store::parse(NATIVE_LINKS).unwrap().header.file_id; | |
| 222 | let (_, target) = page_by_title(NATIVE_LINKS, "Link target"); | |
| 223 | let mut target_paragraphs = target.clone(); | |
| 224 | // OneNote rewrote the target outline after linking; the link keeps the paragraph | |
| 225 | // identity it saw then (n 28), whose page half is the current paragraph's. | |
| 226 | let paragraph = ExGuid { | |
| 227 | guid: body_paragraphs(&mut target_paragraphs)[0].id.guid, | |
| 228 | n: 28, | |
| 229 | }; | |
| 230 | let (_, mut source) = page_by_title(NATIVE_LINKS, "Read about Rust the Rust site"); | |
| 231 | let stored = field_codes(&mut source); | |
| 232 | let page = LinkTarget::Page { | |
| 233 | identity: target.identity.unwrap(), | |
| 234 | title: &target.title, | |
| 235 | }; | |
| 236 | let object = LinkTarget::Object { | |
| 237 | identity: target.identity.unwrap(), | |
| 238 | title: &target.title, | |
| 239 | object: paragraph, | |
| 240 | }; | |
| 241 | assert_eq!( | |
| 242 | stored, | |
| 243 | [ | |
| 244 | "https://example.invalid/rust".to_owned(), | |
| 245 | internal_link(section, NATIVE_BASE_PATH, page), | |
| 246 | internal_link(section, NATIVE_BASE_PATH, object), | |
| 247 | internal_link(section, NATIVE_BASE_PATH, LinkTarget::Section), | |
| 248 | ] | |
| 249 | ); | |
| 250 | assert_eq!( | |
| 251 | internal_link( | |
| 252 | [0; 16], | |
| 253 | "p", | |
| 254 | LinkTarget::Page { | |
| 255 | identity: [0; 16], | |
| 256 | title: "A b/c" | |
| 257 | } | |
| 258 | ), | |
| 259 | "onenote:#A%20b%2Fc&section-id={00000000-0000-0000-0000-000000000000}&page-id={00000000-0000-0000-0000-000000000000}&end&base-path=p" | |
| 260 | ); | |
| 261 | } | |
| 262 | ||
| 263 | /// `ONESTORE_INTERNAL_LINK_EXPORT` names a new directory receiving the candidate for a cold | |
| 264 | /// reopen: a section whose first page links to its second page and to a paragraph on it. | |
| 265 | #[test] | |
| 266 | fn a_page_links_to_another_page_and_its_paragraph() { | |
| 267 | let source = onestore::create_section("links.one", "Linking page", "Author").unwrap(); | |
| 268 | let creation = onestore::PageCreation::new(None, Some("Link target"), "Author").unwrap(); | |
| 269 | let with_target = onestore::PreparedEdit::create_page(&source, &creation) | |
| 270 | .unwrap() | |
| 271 | .as_bytes() | |
| 272 | .to_vec(); | |
| 273 | let section = Store::parse(&with_target).unwrap().header.file_id; | |
| 274 | let (_, target) = page_by_title(&with_target, "Link target"); | |
| 275 | let (space, before) = page_by_title(&with_target, "Linking page"); | |
| 276 | let paragraph = target | |
| 277 | .objects | |
| 278 | .iter() | |
| 279 | .find_map(|object| match object { | |
| 280 | PageObject::Outline(outline) if !outline.title => outline.paragraphs.first(), | |
| 281 | _ => None, | |
| 282 | }) | |
| 283 | .map(|p| p.id); | |
| 284 | let base_path = r"C:\one-tests\runs\capture\notebook\links.one"; | |
| 285 | let page_url = internal_link( | |
| 286 | section, | |
| 287 | base_path, | |
| 288 | LinkTarget::Page { | |
| 289 | identity: target.identity.unwrap(), | |
| 290 | title: &target.title, | |
| 291 | }, | |
| 292 | ); | |
| 293 | let mut after = before.clone(); | |
| 294 | let text = &mut body_paragraphs(&mut after)[0].text_mut().unwrap().text; | |
| 295 | let end = text.utf16_offset(text.text().len()).unwrap(); | |
| 296 | let base = text.format_at(end).unwrap().clone(); | |
| 297 | let mut code = base.clone(); | |
| 298 | code.hyperlink = Some(true); | |
| 299 | code.hyperlink_label = Some(true); | |
| 300 | code.hidden = Some(true); | |
| 301 | let mut visible = base.clone(); | |
| 302 | visible.hyperlink = Some(true); | |
| 303 | visible.hyperlink_label = Some(true); | |
| 304 | let mut runs = vec![ | |
| 305 | (" ".to_owned(), base.clone()), | |
| 306 | (format!("\u{fddf}HYPERLINK \"{page_url}\""), code.clone()), | |
| 307 | ("Link target".to_owned(), visible.clone()), | |
| 308 | ]; | |
| 309 | if let Some(paragraph) = paragraph { | |
| 310 | let object_url = internal_link( | |
| 311 | section, | |
| 312 | base_path, | |
| 313 | LinkTarget::Object { | |
| 314 | identity: target.identity.unwrap(), | |
| 315 | title: &target.title, | |
| 316 | object: paragraph, | |
| 317 | }, | |
| 318 | ); | |
| 319 | runs.push((" ".to_owned(), base.clone())); | |
| 320 | runs.push((format!("\u{fddf}HYPERLINK \"{object_url}\""), code)); | |
| 321 | runs.push(("its paragraph".to_owned(), visible)); | |
| 322 | } | |
| 323 | text.apply(Edit { | |
| 324 | range: end..end, | |
| 325 | replacement: Paragraph::from_runs(runs), | |
| 326 | }) | |
| 327 | .unwrap(); | |
| 328 | let written = PreparedEdit::page(&with_target, space, &after, AUTHOR).unwrap(); | |
| 329 | assert_same(written.as_bytes(), space, &after); | |
| 330 | if let Some(directory) = std::env::var_os("ONESTORE_INTERNAL_LINK_EXPORT") { | |
| 331 | let directory = std::path::PathBuf::from(directory); | |
| 332 | std::fs::create_dir(&directory).unwrap(); | |
| 333 | std::fs::write(directory.join("links.one"), written.as_bytes()).unwrap(); | |
| 334 | std::fs::write( | |
| 335 | directory.join("Open Notebook.onetoc2"), | |
| 336 | onestore::create_table_of_contents("Open Notebook.onetoc2", &[("links.one", section)]) | |
| 337 | .unwrap(), | |
| 338 | ) | |
| 339 | .unwrap(); | |
| 340 | } | |
| 341 | } |
crates/onestore/tests/page_tags.rs+9-1| ... | ... | @@ -267,10 +267,18 @@ fn new_definitions_and_tags_publish_on_a_fresh_page() { |
| 267 | 267 | let important = new_id().unwrap(); |
| 268 | 268 | after.definitions.insert(task, definition("Rust task", 3)); |
| 269 | 269 | let mut important_definition = definition("Important", 13); |
| 270 | let Kind::TagDefinition { action_type, .. } = &mut important_definition.kind else { | |
| 270 | let Kind::TagDefinition { | |
| 271 | action_type, | |
| 272 | color, | |
| 273 | highlight, | |
| 274 | .. | |
| 275 | } = &mut important_definition.kind | |
| 276 | else { | |
| 271 | 277 | panic!() |
| 272 | 278 | }; |
| 273 | 279 | *action_type = Some(1); |
| 280 | *color = Some(0x0000_00ff); | |
| 281 | *highlight = Some(0x0000_ffff); | |
| 274 | 282 | after.definitions.insert(important, important_definition); |
| 275 | 283 | body_paragraphs(&mut after)[0] |
| 276 | 284 | .text_mut() |
crates/snowbound/src/accessibility.rs+2| ... | ... | @@ -611,6 +611,7 @@ mod tests { |
| 611 | 611 | .snapshot(); |
| 612 | 612 | title.title = true; |
| 613 | 613 | let page = Page { |
| 614 | identity: None, | |
| 614 | 615 | title: "Header".into(), |
| 615 | 616 | created: None, |
| 616 | 617 | margin_origin: [36.0, 14.4], |
| ... | ... | @@ -700,6 +701,7 @@ mod tests { |
| 700 | 701 | use canvas::gpu::page::PageScene; |
| 701 | 702 | use onestore::page::{Page, PageObject, Unsupported}; |
| 702 | 703 | let page = Page { |
| 704 | identity: None, | |
| 703 | 705 | created: None, |
| 704 | 706 | title: String::new(), |
| 705 | 707 | margin_origin: [0.0; 2], |
crates/snowbound/src/main.rs+4| ... | ... | @@ -2126,6 +2126,7 @@ mod tests { |
| 2126 | 2126 | }); |
| 2127 | 2127 | fields[0].title = true; |
| 2128 | 2128 | let page = Page { |
| 2129 | identity: None, | |
| 2129 | 2130 | title: "Header".into(), |
| 2130 | 2131 | created: Some(1), |
| 2131 | 2132 | margin_origin: [36.0, 14.4], |
| ... | ... | @@ -2229,6 +2230,7 @@ mod tests { |
| 2229 | 2230 | ); |
| 2230 | 2231 | } |
| 2231 | 2232 | let page = onestore::page::Page { |
| 2233 | identity: None, | |
| 2232 | 2234 | title: String::new(), |
| 2233 | 2235 | created: None, |
| 2234 | 2236 | margin_origin: [0.0; 2], |
| ... | ... | @@ -2966,6 +2968,7 @@ mod tests { |
| 2966 | 2968 | objects.reverse(); |
| 2967 | 2969 | } |
| 2968 | 2970 | let page = Page { |
| 2971 | identity: None, | |
| 2969 | 2972 | created: None, |
| 2970 | 2973 | title: String::new(), |
| 2971 | 2974 | margin_origin: [0.0; 2], |
| ... | ... | @@ -3143,6 +3146,7 @@ mod tests { |
| 3143 | 3146 | ) |
| 3144 | 3147 | .unwrap(); |
| 3145 | 3148 | let page = Page { |
| 3149 | identity: None, | |
| 3146 | 3150 | created: None, |
| 3147 | 3151 | title: String::new(), |
| 3148 | 3152 | margin_origin: [0.0; 2], |
tools/native/page-link.ps1 created+63| ... | ... | @@ -0,0 +1,63 @@ |
| 1 | param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) | |
| 2 | Set-StrictMode -Version Latest | |
| 3 | $ErrorActionPreference = 'Stop' | |
| 4 | & "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost | |
| 5 | $namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' | |
| 6 | $app = New-Object -ComObject OneNote.Application | |
| 7 | $notebookId = '' | |
| 8 | try { | |
| 9 | $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) | |
| 10 | $sectionId = '' | |
| 11 | $app.OpenHierarchy('links.one', $notebookId, [ref]$sectionId, 0) | |
| 12 | $deadline = [DateTime]::UtcNow.AddSeconds(30) | |
| 13 | $page = $null | |
| 14 | $pages = @() | |
| 15 | do { | |
| 16 | $hierarchy = '' | |
| 17 | $app.GetHierarchy($sectionId, 4, [ref]$hierarchy, 1) | |
| 18 | [xml]$tree = $hierarchy | |
| 19 | $pages = @($tree.SelectNodes('//*[local-name()="Page"]')) | |
| 20 | if ($pages.Count -eq 1) { | |
| 21 | $content = '' | |
| 22 | $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1) | |
| 23 | [xml]$page = $content | |
| 24 | if (@($page.SelectNodes('//*[local-name()="OE"]')).Count -ge 1) { break } | |
| 25 | } | |
| 26 | Start-Sleep -Milliseconds 250 | |
| 27 | } while ([DateTime]::UtcNow -lt $deadline) | |
| 28 | if ($pages.Count -ne 1) { throw 'Expected one page in the section.' } | |
| 29 | [IO.File]::WriteAllText((Join-Path $Root 'before.xml'), $content, [Text.Encoding]::UTF8) | |
| 30 | $targetId = '' | |
| 31 | $app.CreateNewPage($sectionId, [ref]$targetId, 0) | |
| 32 | $target = '<one:Page xmlns:one="' + $namespace + '" ID="' + $targetId + '"><one:Title><one:OE><one:T><![CDATA[Link target]]></one:T></one:OE></one:Title><one:Outline><one:Position x="36" y="86"/><one:Size width="300" height="30"/><one:OEChildren><one:OE><one:T><![CDATA[Target paragraph]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page>' | |
| 33 | $app.UpdatePageContent($target, [DateTime]::MinValue, 1, $true) | |
| 34 | $targetContent = '' | |
| 35 | $app.GetPageContent($targetId, [ref]$targetContent, 3, 1) | |
| 36 | [xml]$targetPage = $targetContent | |
| 37 | $targetParagraph = $targetPage.SelectSingleNode('//*[local-name()="Outline"]/*[local-name()="OEChildren"]/*[local-name()="OE"]').GetAttribute('objectID') | |
| 38 | $pageLink = '' | |
| 39 | $app.GetHyperlinkToObject($targetId, '', [ref]$pageLink) | |
| 40 | $paragraphLink = '' | |
| 41 | $app.GetHyperlinkToObject($targetId, $targetParagraph, [ref]$paragraphLink) | |
| 42 | $sectionLink = '' | |
| 43 | $app.GetHyperlinkToObject($sectionId, '', [ref]$sectionLink) | |
| 44 | @{ page = $pageLink; paragraph = $paragraphLink; section = $sectionLink; target = $targetId; target_paragraph = $targetParagraph } | ConvertTo-Json | Set-Content (Join-Path $Root 'links.json') -Encoding UTF8 | |
| 45 | $children = $page.SelectSingleNode('//*[local-name()="Outline"]/*[local-name()="OEChildren"]') | |
| 46 | foreach ($entry in @(@('Page link', $pageLink), @('Paragraph link', $paragraphLink), @('Section link', $sectionLink))) { | |
| 47 | $oe = $page.CreateElement('one', 'OE', $namespace) | |
| 48 | $t = $page.CreateElement('one', 'T', $namespace) | |
| 49 | [void]$t.AppendChild($page.CreateCDataSection('<a href="' + [Security.SecurityElement]::Escape($entry[1]) + '">' + $entry[0] + '</a>')) | |
| 50 | [void]$oe.AppendChild($t) | |
| 51 | [void]$children.AppendChild($oe) | |
| 52 | } | |
| 53 | [IO.File]::WriteAllText((Join-Path $Root 'update.xml'), $page.OuterXml, [Text.Encoding]::UTF8) | |
| 54 | $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false) | |
| 55 | $app.SyncHierarchy($notebookId) | |
| 56 | $app.CloseNotebook($notebookId, $false) | |
| 57 | $notebookId = '' | |
| 58 | } finally { | |
| 59 | if ($notebookId) { $app.CloseNotebook($notebookId, $false) } | |
| 60 | [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) | |
| 61 | [GC]::Collect() | |
| 62 | [GC]::WaitForPendingFinalizers() | |
| 63 | } |
tools/test_link_edit.py+26| ... | ... | @@ -1,3 +1,5 @@ |
| 1 | import json | |
| 2 | import re | |
| 1 | 3 | from pathlib import Path |
| 2 | 4 | import runpy |
| 3 | 5 | import shutil |
| ... | ... | @@ -23,6 +25,30 @@ class LinkEditTest(unittest.TestCase): |
| 23 | 25 | self.assertEqual([' '.join(text.split()) for text in texts], |
| 24 | 26 | ['Read about Rust <a href="https://example.invalid/rust">the Rust site</a>']) |
| 25 | 27 | |
| 28 | def test_onenote_links_between_pages_use_stored_identities(self): | |
| 29 | with TemporaryDirectory() as temporary: | |
| 30 | read = Path(temporary) / 'read' | |
| 31 | shutil.copytree(FIXTURE / 'native-links/read', read) | |
| 32 | compare(FIXTURE / 'native-links/notebook', read) | |
| 33 | links = json.loads((FIXTURE / 'native-links/links.json').read_text(encoding='utf-8-sig')) | |
| 34 | page, = (ET.parse(path).getroot() for path in sorted((FIXTURE / 'native-links/read').glob('page-*.xml')) | |
| 35 | if ET.parse(path).getroot().get('name') == 'Read about Rust the Rust site') | |
| 36 | hrefs = re.findall(r'href="([^"]*)"', ''.join(oe.find('one:T', ns).text for oe in page.iter('{%s}OE' % ns['one']) if oe.find('one:T', ns) is not None)) | |
| 37 | section = re.search(r'section-id=(\{[^}]*\})', links['page']).group(1) | |
| 38 | page_id = re.search(r'page-id=(\{[^}]*\})', links['page']).group(1) | |
| 39 | self.assertEqual(hrefs[1], 'onenote:#Link%%20target&amp;section-id=%s&amp;page-id=%s&amp;end&amp;base-path=C:\\one-tests\\runs\\capture\\notebook\\links.one' % (section, page_id)) | |
| 40 | self.assertEqual(hrefs[3], 'onenote:#section-id=%s&amp;end&amp;base-path=C:\\one-tests\\runs\\capture\\notebook\\links.one' % section) | |
| 41 | ||
| 42 | def test_rust_internal_links_render_natively(self): | |
| 43 | with TemporaryDirectory() as temporary: | |
| 44 | read = Path(temporary) / 'read' | |
| 45 | shutil.copytree(FIXTURE / 'internal/cold/read', read) | |
| 46 | compare(FIXTURE / 'internal/candidate', read) | |
| 47 | pages = {ET.parse(path).getroot().get('name'): ET.parse(path).getroot() for path in sorted((FIXTURE / 'internal/cold/read').glob('page-*.xml'))} | |
| 48 | page = next(page for name, page in pages.items() if name.startswith('Linking page')) | |
| 49 | text = ' '.join(' '.join(oe.find('one:T', ns).text.split()) for oe in page.iter('{%s}OE' % ns['one']) if oe.find('one:T', ns) is not None) | |
| 50 | self.assertRegex(text, r'^Linking page <a href="onenote:#Link%20target&amp;section-id=\{[0-9A-F-]{36}\}&amp;page-id=\{[0-9A-F-]{36}\}&amp;end&amp;base-path=C:\\one-tests\\runs\\capture\\notebook\\links.one">Link target</a>$') | |
| 51 | ||
| 26 | 52 | |
| 27 | 53 | if __name__ == '__main__': |
| 28 | 54 | unittest.main() |
tools/test_tag_edit.py+3-2| ... | ... | @@ -20,8 +20,9 @@ class TagEditTest(unittest.TestCase): |
| 20 | 20 | shutil.copytree(FIXTURE / 'cold/read', read) |
| 21 | 21 | compare(FIXTURE / 'candidate', read) |
| 22 | 22 | page, = (ET.parse(path).getroot() for path in sorted((FIXTURE / 'cold/read').glob('page-*.xml'))) |
| 23 | definitions = [(d.get('index'), d.get('type'), d.get('symbol'), d.get('name')) for d in page.findall('one:TagDef', ns)] | |
| 24 | self.assertEqual(definitions, [('0', '0', '3', 'Rust task'), ('1', '1', '13', 'Important')]) | |
| 23 | definitions = [(d.get('index'), d.get('type'), d.get('symbol'), d.get('name'), d.get('fontColor'), d.get('highlightColor')) | |
| 24 | for d in page.findall('one:TagDef', ns)] | |
| 25 | self.assertEqual(definitions, [('0', '0', '3', 'Rust task', 'automatic', 'none'), ('1', '1', '13', 'Important', '#FF0000', '#FFFF00')]) | |
| 25 | 26 | tagged = [([(t.get('index'), t.get('completed'), t.get('completionDate') is not None) for t in oe.findall('one:Tag', ns)], |
| 26 | 27 | re.sub(r'<[^>]*>', '', oe.find('one:T', ns).text or '')) |
| 27 | 28 | for oe in page.iter('{%s}OE' % ns['one']) if oe.find('one:T', ns) is not None] |