From bcb13ba046299393ccadffa70cc2c57963ffc3ce Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sat, 12 Sep 2026 15:29:25 -0700 Subject: [PATCH] feat: build internal links from page and section identities; tag colours gated 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§ion-id={section file identity}&page-id={page notebook-management identity}&end&base-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.1 --- corpus/link-edit/README.md | 22 +++ .../internal/candidate/Open Notebook.onetoc2 | Bin 0 -> 1604 bytes corpus/link-edit/internal/candidate/links.one | Bin 0 -> 9200 bytes corpus/link-edit/internal/cold/commands.jsonl | 3 + corpus/link-edit/internal/cold/machine.json | 1 + .../cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 3764 bytes .../internal/cold/notebook/links.one | Bin 0 -> 9200 bytes .../internal/cold/read/environment.json | 7 + .../internal/cold/read/hierarchy.xml | 2 + .../link-edit/internal/cold/read/page-000.xml | 2 + .../link-edit/internal/cold/read/page-001.xml | 3 + .../internal/cold/read/payloads.json | 3 + corpus/link-edit/internal/cold/run.json | 18 +++ .../link-edit/internal/cold/scripts/cold.ps1 | 27 ++++ .../link-edit/internal/cold/scripts/read.ps1 | 142 +++++++++++++++++ corpus/link-edit/internal/cold/source.json | 14 ++ corpus/link-edit/internal/cold/teardown.json | 1 + corpus/link-edit/native-links/before.xml | 3 + corpus/link-edit/native-links/commands.jsonl | 4 + corpus/link-edit/native-links/links.json | 7 + corpus/link-edit/native-links/machine.json | 1 + .../notebook/Open Notebook.onetoc2 | Bin 0 -> 3764 bytes .../link-edit/native-links/notebook/links.one | Bin 0 -> 19416 bytes .../native-links/read/environment.json | 7 + .../link-edit/native-links/read/hierarchy.xml | 2 + .../link-edit/native-links/read/page-000.xml | 6 + .../link-edit/native-links/read/page-001.xml | 2 + .../link-edit/native-links/read/payloads.json | 3 + corpus/link-edit/native-links/run.json | 19 +++ .../link-edit/native-links/scripts/author.ps1 | 63 ++++++++ .../link-edit/native-links/scripts/cold.ps1 | 27 ++++ .../link-edit/native-links/scripts/read.ps1 | 142 +++++++++++++++++ corpus/link-edit/native-links/source.json | 14 ++ corpus/link-edit/native-links/teardown.json | 1 + corpus/link-edit/native-links/update.xml | 2 + corpus/tag-edit/README.md | 5 +- .../tag-edit/candidate/Open Notebook.onetoc2 | Bin 1596 -> 1596 bytes corpus/tag-edit/candidate/tags.one | Bin 8352 -> 8368 bytes corpus/tag-edit/cold/commands.jsonl | 2 +- corpus/tag-edit/cold/machine.json | 2 +- .../cold/notebook/Open Notebook.onetoc2 | Bin 3748 -> 3748 bytes corpus/tag-edit/cold/notebook/tags.one | Bin 8352 -> 8368 bytes corpus/tag-edit/cold/read/environment.json | 2 +- corpus/tag-edit/cold/read/hierarchy.xml | 2 +- corpus/tag-edit/cold/read/page-000.xml | 2 +- corpus/tag-edit/cold/source.json | 10 +- crates/canvas/src/editor.rs | 4 + crates/canvas/src/gpu/page.rs | 7 + crates/notebook/README.md | 5 +- crates/notebook/src/merge.rs | 1 + crates/notebook/src/session.rs | 6 + crates/onestore/README.md | 2 +- crates/onestore/src/page/link.rs | 77 +++++++++ crates/onestore/src/page/mod.rs | 51 ++++-- crates/onestore/tests/page_links.rs | 150 +++++++++++++++++- crates/onestore/tests/page_tags.rs | 10 +- crates/snowbound/src/accessibility.rs | 2 + crates/snowbound/src/main.rs | 4 + tools/native/page-link.ps1 | 63 ++++++++ tools/test_link_edit.py | 26 +++ tools/test_tag_edit.py | 5 +- 61 files changed, 952 insertions(+), 34 deletions(-) create mode 100644 corpus/link-edit/internal/candidate/Open Notebook.onetoc2 create mode 100644 corpus/link-edit/internal/candidate/links.one create mode 100644 corpus/link-edit/internal/cold/commands.jsonl create mode 100644 corpus/link-edit/internal/cold/machine.json create mode 100644 corpus/link-edit/internal/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/link-edit/internal/cold/notebook/links.one create mode 100644 corpus/link-edit/internal/cold/read/environment.json create mode 100644 corpus/link-edit/internal/cold/read/hierarchy.xml create mode 100644 corpus/link-edit/internal/cold/read/page-000.xml create mode 100644 corpus/link-edit/internal/cold/read/page-001.xml create mode 100644 corpus/link-edit/internal/cold/read/payloads.json create mode 100644 corpus/link-edit/internal/cold/run.json create mode 100644 corpus/link-edit/internal/cold/scripts/cold.ps1 create mode 100644 corpus/link-edit/internal/cold/scripts/read.ps1 create mode 100644 corpus/link-edit/internal/cold/source.json create mode 100644 corpus/link-edit/internal/cold/teardown.json create mode 100644 corpus/link-edit/native-links/before.xml create mode 100644 corpus/link-edit/native-links/commands.jsonl create mode 100644 corpus/link-edit/native-links/links.json create mode 100644 corpus/link-edit/native-links/machine.json create mode 100644 corpus/link-edit/native-links/notebook/Open Notebook.onetoc2 create mode 100644 corpus/link-edit/native-links/notebook/links.one create mode 100644 corpus/link-edit/native-links/read/environment.json create mode 100644 corpus/link-edit/native-links/read/hierarchy.xml create mode 100644 corpus/link-edit/native-links/read/page-000.xml create mode 100644 corpus/link-edit/native-links/read/page-001.xml create mode 100644 corpus/link-edit/native-links/read/payloads.json create mode 100644 corpus/link-edit/native-links/run.json create mode 100644 corpus/link-edit/native-links/scripts/author.ps1 create mode 100644 corpus/link-edit/native-links/scripts/cold.ps1 create mode 100644 corpus/link-edit/native-links/scripts/read.ps1 create mode 100644 corpus/link-edit/native-links/source.json create mode 100644 corpus/link-edit/native-links/teardown.json create mode 100644 corpus/link-edit/native-links/update.xml create mode 100644 crates/onestore/src/page/link.rs create mode 100644 tools/native/page-link.ps1 diff --git a/corpus/link-edit/README.md b/corpus/link-edit/README.md index 8523c8e2e4e58af232847a390d61d65e1e365cbb..bb18bc40a59103567d2afa9ae5cfd08f99e1af51 100644 --- a/corpus/link-edit/README.md +++ b/corpus/link-edit/README.md @@ -15,3 +15,25 @@ embedded objects and runs with associated data stay refused. `ONESTORE_LINK_EXPORT` set to a new absolute directory while running the test, then cold-open it with `tools/native_runner.py OUTPUT COLD --expected-pages 1 --collect-notebook`. + +## Internal links + +`native-links/` is OneNote 2010 adding links to a page, to a paragraph on it +and to the section on the Rust-authored page above through the COM API +(`tools/native/page-link.ps1`; `links.json` holds the `onenote:///…` URLs +`GetHyperlinkToObject` returned, `update.xml` the submitted page). In +`notebook/`, OneNote stored each as a `HYPERLINK` field code with the +relative form `onenote:#Link%20target§ion-id={section file identity} +&page-id={page notebook-management identity}&end&base-path=
`; +a paragraph link ends with `&object-id={paragraph identity}&n` instead of +`&end`, and a section link has neither title nor page. OneNote rewrote the +target outline after linking, so its own paragraph link names an identity +(`n` 28) the current outline no longer holds. + +`internal/candidate` is the writer's output for +`a_page_links_to_another_page_and_its_paragraph` in +`crates/onestore/tests/page_links.rs`: a section created in Rust with a +second page, whose first page links to that page and to its first paragraph +with URLs built by `onestore::page::link::internal_link`. `internal/cold` is +its cold read with both links. Regenerate with +`ONESTORE_INTERNAL_LINK_EXPORT` and cold-open with `--expected-pages 2`. diff --git a/corpus/link-edit/internal/candidate/Open Notebook.onetoc2 b/corpus/link-edit/internal/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..706b7e2eb20b7d88a33f1a772e96c0301474b991 GIT binary patch literal 1604 zcmZ3e|KIuM`!b(-j|5(YE0x7p-qa0J?%nC!xyp-u#_HuC8PI_J-B|+C6Tf;cy&yjE z?TK|aq=E9t7^eI`5P;Z>FaanH6Gsz_wnK=uTi$s7phar<8k0G0(m_d&rAIM4t ziXn0}P*npLkYHr^Cc~ft%Xth8N4|Xjy0I#3i6Brl2#6FgG<=(-z$3fU%c-OMsmIjU zJojO$rC%^KIIfs?$~8*et>e0Jon3bA1DG5{Hzo;@R}^7rU{FZ_52gW?I~DUf@afEZ*Chz7ZF*|z#A$a(;J0?`Zr literal 0 HcmV?d00001 diff --git a/corpus/link-edit/internal/candidate/links.one b/corpus/link-edit/internal/candidate/links.one new file mode 100644 index 0000000000000000000000000000000000000000..9cc5dedcc8cdea93cd116113909fa5aeeca20230 GIT binary patch literal 9200 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 literal 0 HcmV?d00001 diff --git a/corpus/link-edit/internal/cold/notebook/links.one b/corpus/link-edit/internal/cold/notebook/links.one new file mode 100644 index 0000000000000000000000000000000000000000..c2daf85762bee9b487a48667250454d1faa5a9ed GIT binary patch literal 9200 zcmeHM4Nz6b6OU;L{^D?v*dG}ZbSYtpnOF-|m{Bxcl%9VOMI8cmbf@4I*R@!sX~Y-*=7&TyE0 z_ug~Pp51fyoU><_i(@A)SblbAcJ;n-W%Yg5obLAYlA40Af1I;)X~E6tgjq9%2~s{> z7u;>djZr)5JFjRsReknh&^?=CzXRaAze32T!Yg$7xwx@CUpO#g>F&{O*Y4SQJ^k~= z{+9y{Y9StuAmcq352wVt43zu#T2J?@{&MJ>g|2A}wx=!b)MirU#>tyoOY+Ot_rJNl z?q}H>#*Kab)1uY2-o2WB)%MxaLA9pT7EFr;?tcsT3868>{6)M?*rwY=7rRZw1Ou~u zdwO|8k7hjzBtA@`;{ke&3apIp}-q<()iS6Vj)H;P$`P&X7W^U}8 zy4SU^a&p+9o0K0NEVLNV$a8#c;JMg{FEW;&j2zVC$${0xljpB*J^x(FMrY<99LY&l zpHwxR_>$OAARwULz zv|x@S;9F!J9I)tk*s*0wpN>QVLW_lFo-YLuqD~2IbMIZff6#M&s%vil3ll{|6%~atL0>aiE$xSyXyKsyiSF=EFKcLRbl?2~{;T})4*Zh7Oz#Qb zdnR?yqSwZ(A5~uD@0?KG6}T~ChA0v<_1~w2T@;Hc;wdp56y?PnL3t%!X$h?}o&jP$ zWHUxeBZX}k+K|}ye@O0!(9%SyC=rEXmY9t%-y?wmK)GG;Z0DBiZxcZw&?OWx6^7Og z&4#AN@oQ&vV{d?#Vl+qEy2^+Y#G23|fach2ZFIS`8ReEm3VS(Pd+6Yn#}|Db-N0|? z7me{dMTwYiZ6cUt-KSSoN+Lt%X?La=U(FY_mT&8wK0Tq9q7tQo_XMiBo=}Y zh8FLc2V&a-1IR<;d;>juYLe&xfvyweNvxMd<{pmwpeL~%ml%+V#>IR{5#Oj4E{7!A zLEyUh3B(wKF)lv*Os_xPaCNwySk*fv@)}|Uvd(dx2*JK-m)`A0^!7sR)Tin8G-ZS%5dcx24;t6ss)gF1$ezq`R@7u z*ROVs3b}N6_|BmE@P+Na(2Q!(H<1$uSt}-ur?i7J|T7_1+igDu7fp@_FqGg_0N#0$=;7 z?9R$^tt8q(puc!+8g(9G=^S30#w|&7fIu61Z5o-B1+b&hU3_jDg_1cHjPwuprm^wy zBIoRp1x$1Wj{a5w>ptGVG9F4yQp8HIloM=5zR-V$_K9l-t z&2{c2=Gl1G$7Y&$eQcE{mA>4;3EXGQQaE?+GcMW|aLssF=3S69-*vEAy05tQnFsYV z66eMi;669^5Ian+Ua-008p1_Zrukn^=VuM#f=W3NmayAI!0tK3}dt*lGz z$o|E0<~8hZVXfx+m)G5@G|s4;i7)+Fhj6~Ey~?EDIPh(@C3&pmFq#;q%wMc;7}s12 z`gc1tt~Z|)%|y-dSlUqD@iSM#Bk?3XNe$vVm|zqwngH7NVV$8z%lvISZ!&|-oA zy9GRRfE@fT`>**Pss^F2&LO;w^1O$#i;us_6#1x+)5TbDjS@K`3%MW~H%mpRl8f*x z!E>nS>6Y<5@d$P}&+|+#F$X+Tft#WCi^Fr8NEQolPn9SJLUsb4L&QiCCx+uI9ba*1 z!}0eB-qJ*d7zBD8ohzIa8jT)ar$_?V@t*M5ojN0` zvlq)x@B6_P54it4K-F*DuL-Q*HXR-P&FLjqvbE{I-=8#LPkZj^o^|VgTED3}EmIwW zP(bh+#H()2loj)3jKU(qeYEslw}KP-$@AVdJW9Ai(IU{Azk^l!vMwP|-$TcauI{;~ zH2am@M=z}`uS)rE^;Pxi_lGVz@L2NBFu89Kz~h|d-AnGbz`!sN%U&N`~93fSDj0e zs!xU!Z}oo$;*IlWyv!UtZ(3#N=FW@iylIy!b_D&cvR!lidEVrCwcd`7fKzBz-gnpj R(vnBe-)i-I_4j?={WqoE;(Y)B literal 0 HcmV?d00001 diff --git a/corpus/link-edit/internal/cold/read/environment.json b/corpus/link-edit/internal/cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..86799bad7ebf19b99589758ceb9baa77b641c4d5 --- /dev/null +++ b/corpus/link-edit/internal/cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-A4418B81", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/link-edit/internal/cold/read/hierarchy.xml b/corpus/link-edit/internal/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..a386bc9495f54320ec819f931e8894bec190d797 --- /dev/null +++ b/corpus/link-edit/internal/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/link-edit/internal/cold/read/page-000.xml b/corpus/link-edit/internal/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..350680605480840543665e7b1768927b126eaa20 --- /dev/null +++ b/corpus/link-edit/internal/cold/read/page-000.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/link-edit/internal/cold/read/page-001.xml b/corpus/link-edit/internal/cold/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..932908066288f0d2537155a759c57ccf92856540 --- /dev/null +++ b/corpus/link-edit/internal/cold/read/page-001.xml @@ -0,0 +1,3 @@ + +Link target]]> diff --git a/corpus/link-edit/internal/cold/read/payloads.json b/corpus/link-edit/internal/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/link-edit/internal/cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/link-edit/internal/cold/run.json b/corpus/link-edit/internal/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..46d54750380e3863ca25f56c1b44e8dba711073d --- /dev/null +++ b/corpus/link-edit/internal/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/ilink", + "expected_pages": 2, + "author": null, + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/link-edit/internal/cold/scripts/cold.ps1 b/corpus/link-edit/internal/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/link-edit/internal/cold/scripts/cold.ps1 @@ -0,0 +1,27 @@ +param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } +$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' +if ($CloneHost) { + if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { + throw 'The disposable clone hostname does not match this machine.' + } + New-Item "$key\Options\Paths" -Force | Out-Null + New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null +} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or + -not (Test-Path 'C:\one-tests\profile-original-cache')) { + throw 'Park the personal OneNote profile before resetting the test cache.' +} +$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' +$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) +if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } +New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null +if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } +if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } +New-Item "$key\OpenNotebooks" | Out-Null +New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null diff --git a/corpus/link-edit/internal/cold/scripts/read.ps1 b/corpus/link-edit/internal/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/link-edit/internal/cold/scripts/read.ps1 @@ -0,0 +1,142 @@ +param( + [Parameter(Mandatory=$true)][string]$Root, + [int]$ExpectedPages = -1, + [switch]$UseCurrentCache, + [switch]$Pdf, + [switch]$KeepOpen, + [string]$CloneHost = '' +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +$notebook = Join-Path $root 'notebook' +$output = Join-Path $root 'read' +if (Test-Path $output) { throw 'Choose a new read destination.' } +if ($UseCurrentCache) { + if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { + throw 'Park the personal OneNote profile before reading test notebooks.' + } +} else { + & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost +} +New-Item -ItemType Directory -Path $output | Out-Null +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$failure = $null +try { + $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) + $process = Get-Process ONENOTE + @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; + powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | + ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 + $sections = @() + foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { + $id = '' + $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) + $sections += $id + } + $deadline = [DateTime]::UtcNow.AddSeconds(300) + $previous = '' + $lastChange = '' + $stableSince = [DateTime]::UtcNow + $settled = $false + do { + $pages = @{} + foreach ($section in $sections) { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$xml = $hierarchy + foreach ($node in $xml.SelectNodes('//*[@path]')) { + if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { + throw 'OneNote opened a section outside the copied notebook.' + } + } + foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { + $id = $node.GetAttribute('ID') + $content = '' + $app.GetPageContent($id, [ref]$content, 1, 1) + $pages[$id] = $content + } + } + $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) + if ($signature -ne $previous) { + $lastChange = $previous + $previous = $signature + $stableSince = [DateTime]::UtcNow + } + if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or + ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and + ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { + [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) + $index = 0 + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + $index++ + } + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) + throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." + } + $index = 0 + $payloads = @() + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + if ($Pdf) { + $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) + $app.NavigateTo($id, '', $false) + $app.Publish($id, $pdfPath, 3, '') + if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } + } + [xml]$page = $pages[$id] + foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { + $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) + $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() + [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) + $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); + kind = $file.LocalName; name = $file.GetAttribute('preferredName'); + sha256 = $hash; bytes = $bytes.Length } + } + $index++ + } + [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) + $all = '' + $app.GetHierarchy($notebookId, 4, [ref]$all, 1) + [xml]$finalTree = $all + $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) + if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { + throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' + } + [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) + Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." +} catch { + $failure = $_ + [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + throw +} finally { + try { + try { + if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } + if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { + $app.CloseNotebook($notebookId, $false) + } + } catch { + if ($null -eq $failure) { throw } + [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + } + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + } + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + if (-not $UseCurrentCache -and -not $CloneHost) { + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 + } +} diff --git a/corpus/link-edit/internal/cold/source.json b/corpus/link-edit/internal/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..439edd6ce12404fddb1b88a971314a02bc4b5d5e --- /dev/null +++ b/corpus/link-edit/internal/cold/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1604, + "sha256": "fdee47831417ef5d21624890fe8d9f3d73a4ded56cacbbe2ebb978788786681b", + "mtime_ns": 1789251129798178239 + }, + { + "path": "links.one", + "bytes": 9200, + "sha256": "16b69f8178ce8a23e37ca844b89ae736f4e2c57827205fcfc1e51501b2865d17", + "mtime_ns": 1789251129797893821 + } +] diff --git a/corpus/link-edit/internal/cold/teardown.json b/corpus/link-edit/internal/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/link-edit/internal/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/link-edit/native-links/before.xml b/corpus/link-edit/native-links/before.xml new file mode 100644 index 0000000000000000000000000000000000000000..44442d5bd4b87e8ca11c9dc928e73730bf9a5610 --- /dev/null +++ b/corpus/link-edit/native-links/before.xml @@ -0,0 +1,3 @@ + +the Rust site]]> diff --git a/corpus/link-edit/native-links/commands.jsonl b/corpus/link-edit/native-links/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..8df3611a2444f79f1ff28f677761e8a95975d982 --- /dev/null +++ b/corpus/link-edit/native-links/commands.jsonl @@ -0,0 +1,4 @@ +{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\author.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-66B8F3A0", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-66B8F3A0 -ExpectedPages 2 -UseCurrentCache", "exit": 0, "stdout": "Read 1 sections and 2 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/link-edit/native-links/links.json b/corpus/link-edit/native-links/links.json new file mode 100644 index 0000000000000000000000000000000000000000..bafe9c5da87bf6b9a2d0660f6da5433391a7c2f4 --- /dev/null +++ b/corpus/link-edit/native-links/links.json @@ -0,0 +1,7 @@ +{ + "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", + "section": "onenote:///C:\\one-tests\\runs\\capture\\notebook\\links.one#section-id={F8C0DD10-38DD-49C1-B6E4-B882569883D1}\u0026end", + "target_paragraph": "{1DAB917D-E947-4865-BCEA-2612693D9939}{40}{B0}", + "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", + "target": "{38458021-4A30-4602-B127-7D34E7287AC7}{1}{B0}" +} diff --git a/corpus/link-edit/native-links/machine.json b/corpus/link-edit/native-links/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..fe651dffd9cd953cc29209c679bbbb5f6fb68adc --- /dev/null +++ b/corpus/link-edit/native-links/machine.json @@ -0,0 +1 @@ +{"name": "m6-66b8f3a0", "hostname": "ONE-M6-66B8F3A0"} diff --git a/corpus/link-edit/native-links/notebook/Open Notebook.onetoc2 b/corpus/link-edit/native-links/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..d540de51d26d094b0c6f9e89976991f519245a74 GIT binary patch literal 3764 zcmZ3e|KIuM`!b(-j|5(YE0x7-U%1(NBK?KiMr9et4GSM;FrWeZyR!tOCw}!@dO>{R z+Y{?X^XH>xCxw?S2q*^#t4xs}(Ym4SWei+8C zVpMcA1crPFG_WvCk!6tCDZ?O=4W#!0X#t?8Wcj|W3)!>Mf9K9s;cG-sPGtm&i2!j9 zLncEWLpDP(gC0XZkd+D)gXKg92B4}2E+E0k@J)t61*8W8j(qw4bz@c75CnU?3X$UYh=v6RuFtC7hF)#!GF~kcX9!LjB zHUKIs0g?ragJeKB(g5gXC|d?-7Q+^3nFTV?dzW$3VbMd~F#ihy6@mZ{&@t!^1Q{ma zz%U^LY8BXYpcuHIgO+vZ<_Uug0|Jf-Kz-2i5##}EX0zb16Px*}U#`(R>l4n{XNe+5=8QAS;-_=?Ii2 zLAGz1cMnLy!V=_0kcrYS8XD%6Ee_NAoaDR6@1S?ncZ($;acpVGVbh0=OwM;1LGla? ziXsi5^a3P+K-Xu1^Yx-l4y%$EY~Af9o2+U85eHceVuQ4Ru!cZGLl02T{O?_+cYn@v z+{cw#&iqgkY^9?`L(ddo^s{gQb+PIOH1xOtd0epM0W_c`g`o#Iz<~xbFw|%={E`70 u3+9XxqaiRF0?-gZ&ML6750vjkff$q-HRpc7Xrn^4j#8r`Fd72GFa!V+20L>A literal 0 HcmV?d00001 diff --git a/corpus/link-edit/native-links/notebook/links.one b/corpus/link-edit/native-links/notebook/links.one new file mode 100644 index 0000000000000000000000000000000000000000..7402d0aeb72ccefac32abc23ed45a77e407b6d07 GIT binary patch literal 19416 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 J2aM+b{{o?yg%$t+ literal 0 HcmV?d00001 diff --git a/corpus/link-edit/native-links/read/environment.json b/corpus/link-edit/native-links/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..edaabd17a681dc24965ecefaa363c9ef7e6200b1 --- /dev/null +++ b/corpus/link-edit/native-links/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-66B8F3A0", + "cold": false, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/link-edit/native-links/read/hierarchy.xml b/corpus/link-edit/native-links/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..db716e4b37b4897d6a82df408756ac77eefbde87 --- /dev/null +++ b/corpus/link-edit/native-links/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/link-edit/native-links/read/page-000.xml b/corpus/link-edit/native-links/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..3d49c38a70d4dc7adfe1f2940276ed64b8f1853b --- /dev/null +++ b/corpus/link-edit/native-links/read/page-000.xml @@ -0,0 +1,6 @@ + +the Rust site]]>Page link]]>Paragraph link]]>Section link]]> diff --git a/corpus/link-edit/native-links/read/page-001.xml b/corpus/link-edit/native-links/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..cc8e3206c43b4798a6192be2950c370fda6a9b3a --- /dev/null +++ b/corpus/link-edit/native-links/read/page-001.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/link-edit/native-links/read/payloads.json b/corpus/link-edit/native-links/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/link-edit/native-links/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/link-edit/native-links/run.json b/corpus/link-edit/native-links/run.json new file mode 100644 index 0000000000000000000000000000000000000000..272cd81addb626bc2ce1dd63dea2e27914e20406 --- /dev/null +++ b/corpus/link-edit/native-links/run.json @@ -0,0 +1,19 @@ +{ + "notebook": "/Users/clo/dev/one/corpus/link-edit/candidate", + "expected_pages": 2, + "author": "tools/native/page-link.ps1", + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "author.ps1": "a67883aa9e5ffd80b9709d8558f37184cf05d79b28b35ce0db3b2f8a16b57225", + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/link-edit/native-links/scripts/author.ps1 b/corpus/link-edit/native-links/scripts/author.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..8b4ab67977373c26c922fc7d13c01bb98939624e --- /dev/null +++ b/corpus/link-edit/native-links/scripts/author.ps1 @@ -0,0 +1,63 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $sectionId = '' + $app.OpenHierarchy('links.one', $notebookId, [ref]$sectionId, 0) + $deadline = [DateTime]::UtcNow.AddSeconds(30) + $page = $null + $pages = @() + do { + $hierarchy = '' + $app.GetHierarchy($sectionId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + $pages = @($tree.SelectNodes('//*[local-name()="Page"]')) + if ($pages.Count -eq 1) { + $content = '' + $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1) + [xml]$page = $content + if (@($page.SelectNodes('//*[local-name()="OE"]')).Count -ge 1) { break } + } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if ($pages.Count -ne 1) { throw 'Expected one page in the section.' } + [IO.File]::WriteAllText((Join-Path $Root 'before.xml'), $content, [Text.Encoding]::UTF8) + $targetId = '' + $app.CreateNewPage($sectionId, [ref]$targetId, 0) + $target = '' + $app.UpdatePageContent($target, [DateTime]::MinValue, 1, $true) + $targetContent = '' + $app.GetPageContent($targetId, [ref]$targetContent, 3, 1) + [xml]$targetPage = $targetContent + $targetParagraph = $targetPage.SelectSingleNode('//*[local-name()="Outline"]/*[local-name()="OEChildren"]/*[local-name()="OE"]').GetAttribute('objectID') + $pageLink = '' + $app.GetHyperlinkToObject($targetId, '', [ref]$pageLink) + $paragraphLink = '' + $app.GetHyperlinkToObject($targetId, $targetParagraph, [ref]$paragraphLink) + $sectionLink = '' + $app.GetHyperlinkToObject($sectionId, '', [ref]$sectionLink) + @{ page = $pageLink; paragraph = $paragraphLink; section = $sectionLink; target = $targetId; target_paragraph = $targetParagraph } | ConvertTo-Json | Set-Content (Join-Path $Root 'links.json') -Encoding UTF8 + $children = $page.SelectSingleNode('//*[local-name()="Outline"]/*[local-name()="OEChildren"]') + foreach ($entry in @(@('Page link', $pageLink), @('Paragraph link', $paragraphLink), @('Section link', $sectionLink))) { + $oe = $page.CreateElement('one', 'OE', $namespace) + $t = $page.CreateElement('one', 'T', $namespace) + [void]$t.AppendChild($page.CreateCDataSection('' + $entry[0] + '')) + [void]$oe.AppendChild($t) + [void]$children.AppendChild($oe) + } + [IO.File]::WriteAllText((Join-Path $Root 'update.xml'), $page.OuterXml, [Text.Encoding]::UTF8) + $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' +} finally { + if ($notebookId) { $app.CloseNotebook($notebookId, $false) } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + [GC]::Collect() + [GC]::WaitForPendingFinalizers() +} diff --git a/corpus/link-edit/native-links/scripts/cold.ps1 b/corpus/link-edit/native-links/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/link-edit/native-links/scripts/cold.ps1 @@ -0,0 +1,27 @@ +param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } +$key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' +if ($CloneHost) { + if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { + throw 'The disposable clone hostname does not match this machine.' + } + New-Item "$key\Options\Paths" -Force | Out-Null + New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null +} elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or + -not (Test-Path 'C:\one-tests\profile-original-cache')) { + throw 'Park the personal OneNote profile before resetting the test cache.' +} +$cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' +$parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) +if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } +New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null +if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } +if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } +New-Item "$key\OpenNotebooks" | Out-Null +New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null diff --git a/corpus/link-edit/native-links/scripts/read.ps1 b/corpus/link-edit/native-links/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/link-edit/native-links/scripts/read.ps1 @@ -0,0 +1,142 @@ +param( + [Parameter(Mandatory=$true)][string]$Root, + [int]$ExpectedPages = -1, + [switch]$UseCurrentCache, + [switch]$Pdf, + [switch]$KeepOpen, + [string]$CloneHost = '' +) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$root = [IO.Path]::GetFullPath($Root).TrimEnd('\') +if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { + throw 'Choose a run directly below C:\one-tests\runs.' +} +$notebook = Join-Path $root 'notebook' +$output = Join-Path $root 'read' +if (Test-Path $output) { throw 'Choose a new read destination.' } +if ($UseCurrentCache) { + if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { + throw 'Park the personal OneNote profile before reading test notebooks.' + } +} else { + & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost +} +New-Item -ItemType Directory -Path $output | Out-Null +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +$failure = $null +try { + $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) + $process = Get-Process ONENOTE + @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; + powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | + ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 + $sections = @() + foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { + $id = '' + $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) + $sections += $id + } + $deadline = [DateTime]::UtcNow.AddSeconds(300) + $previous = '' + $lastChange = '' + $stableSince = [DateTime]::UtcNow + $settled = $false + do { + $pages = @{} + foreach ($section in $sections) { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$xml = $hierarchy + foreach ($node in $xml.SelectNodes('//*[@path]')) { + if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { + throw 'OneNote opened a section outside the copied notebook.' + } + } + foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { + $id = $node.GetAttribute('ID') + $content = '' + $app.GetPageContent($id, [ref]$content, 1, 1) + $pages[$id] = $content + } + } + $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) + if ($signature -ne $previous) { + $lastChange = $previous + $previous = $signature + $stableSince = [DateTime]::UtcNow + } + if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or + ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and + ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { + [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) + $index = 0 + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + $index++ + } + $hierarchy = '' + $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) + [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) + throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." + } + $index = 0 + $payloads = @() + foreach ($id in @($pages.Keys | Sort-Object)) { + [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) + if ($Pdf) { + $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) + $app.NavigateTo($id, '', $false) + $app.Publish($id, $pdfPath, 3, '') + if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } + } + [xml]$page = $pages[$id] + foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { + $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) + $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() + [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) + $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); + kind = $file.LocalName; name = $file.GetAttribute('preferredName'); + sha256 = $hash; bytes = $bytes.Length } + } + $index++ + } + [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) + $all = '' + $app.GetHierarchy($notebookId, 4, [ref]$all, 1) + [xml]$finalTree = $all + $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) + if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { + throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' + } + [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) + Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." +} catch { + $failure = $_ + [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + throw +} finally { + try { + try { + if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } + if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { + $app.CloseNotebook($notebookId, $false) + } + } catch { + if ($null -eq $failure) { throw } + [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) + } + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + } + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + if (-not $UseCurrentCache -and -not $CloneHost) { + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 + } +} diff --git a/corpus/link-edit/native-links/source.json b/corpus/link-edit/native-links/source.json new file mode 100644 index 0000000000000000000000000000000000000000..14786084a474f1cc0a6f522bd49c8104ad3627f0 --- /dev/null +++ b/corpus/link-edit/native-links/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1604, + "sha256": "6731c4214437fe1cb6aa347b0004542b7f7e4260f47773bf9ec07c4e894de47c", + "mtime_ns": 1789206821144296891 + }, + { + "path": "links.one", + "bytes": 6128, + "sha256": "0cfb356ed374fe919f73fb73fdbe5d66e0baa484d4119f70074d9f869d6db78a", + "mtime_ns": 1789206821116200533 + } +] diff --git a/corpus/link-edit/native-links/teardown.json b/corpus/link-edit/native-links/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/link-edit/native-links/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/link-edit/native-links/update.xml b/corpus/link-edit/native-links/update.xml new file mode 100644 index 0000000000000000000000000000000000000000..193166474c08eadd81a7e598f5cde65d17128140 --- /dev/null +++ b/corpus/link-edit/native-links/update.xml @@ -0,0 +1,2 @@ +the Rust site]]>Page link]]>Paragraph link]]>Section link]]> \ No newline at end of file diff --git a/corpus/tag-edit/README.md b/corpus/tag-edit/README.md index 7f32604fb23240e27c6ee877292d57fdaf9a8784..f4c030930366abe1bb1aca8c1bd944494caac230 100644 --- a/corpus/tag-edit/README.md +++ b/corpus/tag-edit/README.md @@ -4,7 +4,7 @@ `new_definitions_and_tags_publish_on_a_fresh_page` in `crates/onestore/tests/page_tags.rs`: on a section created in Rust, two tag definitions are added ("Rust task", type 0, symbol 3; "Important", type 1, -symbol 13), the first paragraph gains an open task, a second paragraph a +symbol 13, red text on a yellow highlight), the first paragraph gains an open task, a second paragraph a completed task with creation and completion dates, a third paragraph both tags, and a fourth paragraph none. Tags are property-set arrays on the text 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 requires. `cold/` is a fresh OneNote 2010 read: both `TagDef` entries appear with their -type, symbol and name, the tags read back with their completion state and +type, symbol, name and colours (`fontColor="#FF0000"`, +`highlightColor="#FFFF00"` for the second), the tags read back with their completion state and dates, and the untagged paragraph has none. `tools/test_tag_edit.py` checks this without a VM. Regenerate with `ONESTORE_TAG_EXPORT` set to a new absolute directory while running the test, then cold-open it with diff --git a/corpus/tag-edit/candidate/Open Notebook.onetoc2 b/corpus/tag-edit/candidate/Open Notebook.onetoc2 index e8389f6e1db021270da0b9edd70425d940dbf64e..2c0cd173a96929da9867a717651a72b0ec319c25 100644 GIT binary patch delta 275 zcmdnPvxi4vq5gm8oA1kf<~5{^yNGY)odW@Kbyo-Dw~I+>Y8T%eOr;gSl+ zLEkkMHA>bh9MdLiv3Tk5ez|->@x)%2eXDi{Ja_WsgBd0Lf}!EZ_dVP%8LxZJGH-EK zIjGn&c>#;D2D%271cnBXrfnae%S_JVcG%u7z1R5Y!jF@Gvs4P8E1ulK>VshdNH+`6 K6b!{bSX}^@$Z})= delta 275 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 literal 8352 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 diff --git a/corpus/tag-edit/cold/commands.jsonl b/corpus/tag-edit/cold/commands.jsonl index 0b40bbdbcf63b5876f4029d6235462575970eb6f..a44e0ffa5750ead62217c3115770a1ea314cd0e1 100644 --- a/corpus/tag-edit/cold/commands.jsonl +++ b/corpus/tag-edit/cold/commands.jsonl @@ -1,3 +1,3 @@ {"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} -{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-CFD607C8 -ExpectedPages 1", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-6BD92C98 -ExpectedPages 1", "exit": 0, "stdout": "Read 1 sections and 1 pages.\r\n", "stderr": "", "error": null} {"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/tag-edit/cold/machine.json b/corpus/tag-edit/cold/machine.json index 789e3c21c336c77a53b96bbcb7c2a35cc90ae9ea..cfe68a1084b29f714ce2f6bae198ed5d6c4c2ab3 100644 --- a/corpus/tag-edit/cold/machine.json +++ b/corpus/tag-edit/cold/machine.json @@ -1 +1 @@ -{"name": "m6-cfd607c8", "hostname": "ONE-M6-CFD607C8"} +{"name": "m6-6bd92c98", "hostname": "ONE-M6-6BD92C98"} diff --git a/corpus/tag-edit/cold/notebook/Open Notebook.onetoc2 b/corpus/tag-edit/cold/notebook/Open Notebook.onetoc2 index 24da7710436174fcca0465b4fb65084df42d28a6..fa6f9629a966b7fb791ad01ed69b0c4a9166d4b1 100644 GIT binary patch delta 411 zcmZ1?yF^xDq5gm8oA1kf<~w90xn;5{%00H*1zE!&eo;!K+F+z=ze!i9bm7Ozzga2;&=pT^VfE2O7X;~M0h)p# zKDn1mda?*xlEAr+v&Zx6f?Z}Yl^(aB(P0C0x!mL>Y$Czm?_Y7&;WPA{x#xgH$s&a^ zkWuKa28k_rI&~S7^Ib*|n}I=5q@jU9C7}WA?i=jq8Qdylopy!K<9#%B-M-1(>;PUZCG#G$$%-(O7bCrvAYb zPaQX{EuS^{rRghX1_-#^KkdGX>k-fSQw?U)-l9mf2yM>dD9RR$dDQ z8HMg@u#bFBT$t%}ml4cnP!wrsU{FbD0J}TFDuwyc4`-(qU8%mveYbv1=4MwAcyX3F X=s}66$J&Lv;~F9kT5h&x&twMxQ$C-> diff --git a/corpus/tag-edit/cold/notebook/tags.one b/corpus/tag-edit/cold/notebook/tags.one index d05139e43a9a9b70061e7b44d1feeabdc4c3d978..e06c4884d2d9f368bb651de2c345df771ec5a628 100644 GIT binary patch literal 8368 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 IkJ!S008d*!ApigX literal 8352 zcmeHLc~n%_8NVY)zy*f@iZVk+MT@Cxq<~;ykW~R8RwC9m!5!S7Xb|PZGh%9q#t781 zcnT6C1`QH4@hp7}Rt zR<;hy?=aV#d7Svxjf>wU!vFnvhw|qAygO)XYyPe09gMH6=-MbEJ{|z&t`zGhiHuzFExC zsX52oo3?X(T+m76md;G-{bB-;mzkNSRkz9Jvz#^Dk#DhVA>^i91qrQ)l6lf6nLZq+O$nsM9 z%Q9ExmvBDXiD@YDHS95p&wH$4a*$&8I>j%K$Ga4lh-F02(a|S{obh4C{RQDg@0>mN zfgAP3vvjtUtzibXjxA(s*-Gl;@hi=(#JXew(=g)NvyC(-a^qP&(@m#l$UHp{vd+{( z*|RiL3QJ=d^kr)WV!@&&4|+RV=CjkWL66ej-6)6Lsd-YiQ!IdvUR>29-$th1ZvptK^{i4V8NK^XS>?Aw@z`8Vr2794uFqHJmCCIFN*3aIf3b^!sO)VPWH zK)|O7$>mr6y1p>_$kmYpqW+NbHs{F_-A}n6_>`+`k-~qO0;th)*2fNAG6s}K*5q$1eWq1k;Z0*86E!j8sa0t^qJO*b zA5#x#a;njfJ-b)>rH|k9k+C%Vi{#8JHQUe+o!;NSB`#!XOZ?{ZwO+ob!|Qo{k*C~U zko!>HlKWDXAhs*5WAel%NWQ;><<{8zf-f^dXD<>6qtj+Jonx!$BwI&q39~-UFohEg z$Xpy*xIN4DWrj6Wsijd%_hs73w}NB7X%3Eo1c=A4k}8t@G^6*2o+>*PzxCw2%)zJ6 zXF;|v_EX0zx8I7hSu1bUw?{WD+i^hKv~V@IpY!4!P3iBrgui|wB01sg-aY6?IH>Jy zvFl*$n6WH8NYDbCJ=m2b$+tMK5Q%f?3|h_%^t3*=F&(!95Eq0vYWutWN0AjQVqeSx zzlx;S-%{H_9WU4?K=~39fyjp%^pOx8m~ip#_dZv)BrSS3XhCqNW0y-0`+mlo7dn7-bxvo}592gukB5B02Jn`4UnTsGDJapYz0PY2h| zllhzf9QS6&_4j5>^2y@hYzi|Y}fOCr^4sMeO#AEI5x5MGUN zzi~K$zDdWAedyfwblA@0TXbKzyx`GSeQm|B+74XbT!0~w5f9S0FYze(m1G|j=k!>M z2zu^%GWP5naqe*y?AG3Y+ec=r^?YVp%E7{#?C3hfDwm^`E8gUORmUy&>xjTR`^*l3 zQKhGLJni?&=OyT;{j%AYM^zVl|1G?*;faEMvFopM{j-!y%@4sj=r7?Mf|}}o+gOL} znJFP1s%u05;=n-YcYh5J&7JJYtP<+kSx}E79$=LtVRs*iTk;;r3W8k&&uPs6>IgR0 z>#L&6OzRi*)vmVUr_(JlbM7RMu=>x_oR@x=Rlw~k!bO-RaQa|x54|>`%VmWOZO?n5OhP)(YL!57V7zKMFN(%FyMm-cE~6XTbVNdTvS2) z)G@0E)Vn>X9pF#IswfYUZ=$@#u0?eV)K}Lbm>(ul=PmllOw1O3H!7EaWRe+1&2pjX z!4IT|Njht226+cT!>V$05;t*$TsSU-sJ z5PneS8_%m~rPxm z2MUSF?J!FElI<|%+PT_8EtNA$*If8G^@HmfYCDMZf{aKnV{Zj5 zo{0&;w})J+)~5`Q`||RG>8YWD0EA$;AJ`>UcixRwzTxkWUtD-fpJ|LL{_?BZF6|h9 zwVon;g^U_UE%@UhPj1AE>xlug?|AjwJ^JvnIm^4Mb5`!U+Jm0lA5aEEaW~%hr@aRM z{5>o)<2_+uch&#D$~|@zJIe)L1Db8YU^!$tx|!&exhoqxABh3&rVu$ ziRjetZM=tqm{qjC=#;cIJ|=o>4eV!7eunU&^+l(Pt?}G`RSFV8e-SG7HojKLc&F BFY^EZ diff --git a/corpus/tag-edit/cold/read/environment.json b/corpus/tag-edit/cold/read/environment.json index 594f24f943243c04b579251adfdf3fe8478f9a37..177ce2ecb560bd15f8f883b06aac7ca99a02fa9f 100644 --- a/corpus/tag-edit/cold/read/environment.json +++ b/corpus/tag-edit/cold/read/environment.json @@ -1,7 +1,7 @@ { "powershell": "5.1.14409.1005", "schema": "xs2010", - "hostname": "ONE-M6-CFD607C8", + "hostname": "ONE-M6-6BD92C98", "cold": true, "onenote": "14.0.4763.1000" } diff --git a/corpus/tag-edit/cold/read/hierarchy.xml b/corpus/tag-edit/cold/read/hierarchy.xml index 841314eff1eb2271d34eaff44d07b1c362e7cf4b..12c91df906cb56159555baf4ea8f48cab4c72e78 100644 --- a/corpus/tag-edit/cold/read/hierarchy.xml +++ b/corpus/tag-edit/cold/read/hierarchy.xml @@ -1,2 +1,2 @@  - + diff --git a/corpus/tag-edit/cold/read/page-000.xml b/corpus/tag-edit/cold/read/page-000.xml index 85c699c5520bbfcefe08a894fbf2573a613f1b62..f302dd3c89dfd62306ae6f89806b36e3191b64d5 100644 --- a/corpus/tag-edit/cold/read/page-000.xml +++ b/corpus/tag-edit/cold/read/page-000.xml @@ -1,2 +1,2 @@  - + diff --git a/corpus/tag-edit/cold/source.json b/corpus/tag-edit/cold/source.json index bf8d669db188f0769bb053778a7153c3516f39e4..2c55890abe6d87c5632ff8d714fdbfd461662eb8 100644 --- a/corpus/tag-edit/cold/source.json +++ b/corpus/tag-edit/cold/source.json @@ -2,13 +2,13 @@ { "path": "Open Notebook.onetoc2", "bytes": 1596, - "sha256": "f3ce1472c9be35b1fb52683c5214e62850787e8e13bbd1918fed81fd19a48a65", - "mtime_ns": 1789205536390985808 + "sha256": "4f64a5de0f6a4a1bfc92ec83d50f6cfd986b0c72d803dd9096b277b294b1288f", + "mtime_ns": 1789250567099455316 }, { "path": "tags.one", - "bytes": 8352, - "sha256": "601a8998b691bab66830196b3b139be6ea58d6f29d5bc69b3efa9892e2fc1550", - "mtime_ns": 1789205536390698932 + "bytes": 8368, + "sha256": "bb1715165c52bcc443a012178604d9d1c9b5d4d9fb9c18b6ecb806583db5305d", + "mtime_ns": 1789250567099157565 } ] diff --git a/crates/canvas/src/editor.rs b/crates/canvas/src/editor.rs index d783b2c35fd8d4f7daa9b53025f73e5b2774cd3c..c5d23e9af6f7d47f4ec3e8718815f0018093e6d2 100644 --- a/crates/canvas/src/editor.rs +++ b/crates/canvas/src/editor.rs @@ -114,6 +114,7 @@ pub struct CanvasEditor { #[derive(Default)] struct PageHeader { title: String, + identity: Option<[u8; 16]>, created: Option, margin_origin: [f32; 2], areas: Vec, @@ -699,6 +700,7 @@ impl CanvasEditor { editor.objects = objects; editor.header = PageHeader { title: page.title, + identity: page.identity, created: page.created, margin_origin: page.margin_origin, areas, @@ -767,6 +769,7 @@ impl CanvasEditor { ); Ok(Page { title: self.header.title.clone(), + identity: self.header.identity, created: self .date .as_ref() @@ -4384,6 +4387,7 @@ mod tests { let mut editor = CanvasEditor::from_page( Page { title: "Header".into(), + identity: None, created: None, margin_origin: [36.0, 14.4], definitions: BTreeMap::new(), diff --git a/crates/canvas/src/gpu/page.rs b/crates/canvas/src/gpu/page.rs index 3067f71a5fe3bc961d2ca6f91f66c745203a5733..8fdbbfb399444f14535a9e98f84ce41a1d45df44 100644 --- a/crates/canvas/src/gpu/page.rs +++ b/crates/canvas/src/gpu/page.rs @@ -550,6 +550,7 @@ mod tests { let id = fields[0].id; let source = fields[0].paragraphs.clone(); let page = Page { + identity: None, created: Some(1), title: "Header".into(), margin_origin: [36.0, 14.4], @@ -987,6 +988,7 @@ mod tests { fields[0].min_width = Some(162.0); let title_id = fields[0].id; let page = Page { + identity: None, created: None, title: "Header".into(), margin_origin: [36.0, 14.4], @@ -1060,6 +1062,7 @@ mod tests { }) .collect(); let page = Page { + identity: None, created: None, title: String::new(), margin_origin: [0.0; 2], @@ -1138,6 +1141,7 @@ mod tests { PageObject::Outline(tagged) }; let page = Page { + identity: None, created: None, title: String::new(), margin_origin: [36.0, 14.0], @@ -1208,6 +1212,7 @@ mod tests { layout: Layout::default(), }; let page = Page { + identity: None, created: None, title: String::new(), margin_origin: [0.0; 2], @@ -1383,6 +1388,7 @@ mod tests { .unwrap(); } let page = || Page { + identity: None, created: None, title: String::new(), margin_origin: [0.0; 2], @@ -1504,6 +1510,7 @@ mod tests { .unwrap(); } let page = |width| Page { + identity: None, created: None, title: String::new(), margin_origin: [0.0; 2], diff --git a/crates/notebook/README.md b/crates/notebook/README.md index aaffc0e1b263a86614981ebc249d2c33fd3b278e..cd1073986e9ef784ae93d96983a051ab006d9636 100644 --- a/crates/notebook/README.md +++ b/crates/notebook/README.md @@ -14,7 +14,7 @@ diffs the supplied model against the page stored in the supplied local snapshot, writes the difference into the working image and queues one `Operation::Page(PageIntent { before, after, author })`; `before` is the page the edit started from and is the precondition reconciliation checks. Text, styles, -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, +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, outline layout, insertions and deletions are all differences between `before` and `after`; the library never sees editor operations. @@ -60,7 +60,8 @@ a replica stored under the cache directory, named by the section's document identity so the same file reopens the same queue after a relaunch. A section publishes in the background to the file itself under OneNote-compatible exclusion. `pages()` lists page spaces and titles from the local image, -`page(space)` returns the model to edit, and `save(space, before, after, +`page(space)` returns the model to edit (its `identity` and the section's +`identity()` feed `onestore::page::link::internal_link`), and `save(space, before, after, author)` queues the edited model: `Save::Queued(id)` is durable locally, `Save::Unchanged` means the model equals the stored page, and `Save::Stale` means the stored page no longer matches `before` because the section changed diff --git a/crates/notebook/src/merge.rs b/crates/notebook/src/merge.rs index 136bba9d597c4e21637540a796ab15118c8e26a0..92298e776aac729ba9bc46c0649fa9a113b887a2 100644 --- a/crates/notebook/src/merge.rs +++ b/crates/notebook/src/merge.rs @@ -116,6 +116,7 @@ pub(crate) fn merge(base: &Page, ours: &Page, theirs: &Page) -> Option { } Some(Page { title: theirs.title.clone(), + identity: theirs.identity, created: theirs.created, margin_origin: theirs.margin_origin, objects: result, diff --git a/crates/notebook/src/session.rs b/crates/notebook/src/session.rs index 86f5d0c257d03e68266369aff1ac65cf7adc850d..9bc1b894349395de053b30128038e3487378717f 100644 --- a/crates/notebook/src/session.rs +++ b/crates/notebook/src/session.rs @@ -209,6 +209,12 @@ impl Section { &self.file } + /// The section file identity, which internal links name as `section-id` + /// (`onestore::page::link::internal_link`). + pub fn identity(&self) -> Result<[u8; 16]> { + Ok(Store::parse(&self.replica.snapshot()?)?.header.file_id) + } + /// Page spaces and titles in section order, from the local working image. pub fn pages(&self) -> Result> { let snapshot = self.replica.snapshot()?; diff --git a/crates/onestore/README.md b/crates/onestore/README.md index 3d1e9943f9e4e6cd00c6325acaaf8d5d436ea004..c49355f2254a6d4a3ace5b93fd4254053f99e139 100644 --- a/crates/onestore/README.md +++ b/crates/onestore/README.md @@ -57,7 +57,7 @@ harness also accepts `--client-profile release`. | `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules | | `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | | `OutlineEdit`, `PreparedEdit::outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content | -| `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 | +| `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 | | `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically | | `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter | | `read_file` | Read a snapshot under whole-file exclusion | diff --git a/crates/onestore/src/page/link.rs b/crates/onestore/src/page/link.rs new file mode 100644 index 0000000000000000000000000000000000000000..c2a84e773fdbdd031e1701d869c3cc17e78eeb3f --- /dev/null +++ b/crates/onestore/src/page/link.rs @@ -0,0 +1,77 @@ +//! Links between pages, paragraphs and sections of the same notebook, in the form OneNote +//! stores them: a `HYPERLINK` field code whose URL names the section by its file identity +//! and the page by its notebook-management identity, with the section path as `base-path`. + +use crate::ExGuid; + +/// What an internal link opens. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LinkTarget<'a> { + Section, + Page { + identity: [u8; 16], + title: &'a str, + }, + /// A paragraph or other object on a page, named by its stored identity. + Object { + identity: [u8; 16], + title: &'a str, + object: ExGuid, + }, +} + +/// The URL OneNote 2010 stores for a link into the section whose file identity is `section` +/// and whose file is at `base_path` (the path as the linking client sees it): +/// `onenote:#Title§ion-id={…}&page-id={…}&end&base-path=…`. +pub fn internal_link(section: [u8; 16], base_path: &str, target: LinkTarget<'_>) -> String { + let guid = |bytes: [u8; 16]| { + ExGuid { guid: bytes, n: 0 } + .to_string() + .split(',') + .next() + .unwrap() + .to_owned() + }; + let mut url = String::from("onenote:#"); + match target { + LinkTarget::Section => {} + LinkTarget::Page { title, .. } | LinkTarget::Object { title, .. } => { + url.push_str(&encoded(title)); + url.push('&'); + } + } + url.push_str(&format!("section-id={}", guid(section))); + match target { + LinkTarget::Section => url.push_str("&end"), + LinkTarget::Page { identity, .. } => { + url.push_str(&format!("&page-id={}&end", guid(identity))); + } + LinkTarget::Object { + identity, object, .. + } => { + url.push_str(&format!( + "&page-id={}&object-id={}&{}", + guid(identity), + guid(object.guid), + object.n + )); + } + } + url.push_str("&base-path="); + url.push_str(base_path); + url +} + +/// Percent-encodes a page title the way OneNote does in a link fragment. +fn encoded(title: &str) -> String { + let mut out = String::new(); + for byte in title.bytes() { + match byte { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + out.push(byte as char); + } + _ => out.push_str(&format!("%{byte:02X}")), + } + } + out +} diff --git a/crates/onestore/src/page/mod.rs b/crates/onestore/src/page/mod.rs index 8322927c7389448b2edc12a9272822548c5a7fef..789da80149c8ed6d4967acc1095a850ca7d43ae3 100644 --- a/crates/onestore/src/page/mod.rs +++ b/crates/onestore/src/page/mod.rs @@ -10,6 +10,7 @@ use std::{ sync::Arc, }; +pub mod link; pub mod text; pub(crate) mod write; pub use text::Paragraph; @@ -24,6 +25,8 @@ pub enum DateField { #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub struct Page { pub title: String, + /// The page's notebook-management identity, which internal links name as `page-id`. + pub identity: Option<[u8; 16]>, /// FILETIME ticks from the page's TopologyCreationTimeStamp. pub created: Option, pub margin_origin: [f32; 2], @@ -343,14 +346,21 @@ impl Page { else { return Err(invalid("Canvas root is not a page")); }; + let metadata = revision + .roots + .get(&2) + .and_then(|id| revision.nodes.get(id)) + .filter(|node| matches!(node.kind, Kind::Metadata { .. })) + .and_then(|node| node.extra.first()); let mut page = Self { title: page_title(revision, id).unwrap_or_default().to_owned(), - created: revision - .roots - .get(&2) - .and_then(|id| revision.nodes.get(id)) - .filter(|node| matches!(node.kind, Kind::Metadata { .. })) - .and_then(|node| node.extra.first()) + identity: metadata + .and_then(|fields| fields.iter().find(|field| field.id == 0x1c001c30)) + .and_then(|field| match field.value { + FieldValue::Bytes(bytes) => bytes.try_into().ok(), + _ => None, + }), + created: metadata .and_then(|fields| fields.iter().find(|field| field.id == 0x18001c65)) .map(|field| { let FieldValue::Bytes(bytes) = field.value else { @@ -847,7 +857,7 @@ impl PageParagraph { #[cfg(test)] mod tests { use super::*; - use crate::document::{Element, TextRun}; + use crate::document::{Element, Field, FieldValue, TextRun}; fn id(n: u32) -> ExGuid { ExGuid { @@ -856,6 +866,8 @@ mod tests { } } + const IDENTITY: [u8; 16] = [7; 16]; + fn element(kind: Kind<'_>) -> Element<'_> { Element { jcid: 0, @@ -918,10 +930,14 @@ mod tests { name: Some("Body".into()), }); style.format.font_size = Some(12.0); - let metadata = element(Kind::Metadata { + let mut metadata = element(Kind::Metadata { title: Some("Page title".into()), level: None, }); + metadata.extra = vec![vec![Field { + id: 0x1c001c30, + value: FieldValue::Bytes(&IDENTITY), + }]]; Revision { roots: BTreeMap::from([(2, id(6))]), nodes: BTreeMap::from([ @@ -1183,14 +1199,19 @@ mod tests { #[test] fn creation_time_comes_from_page_metadata_and_date_role_from_the_title_child() { - use crate::document::{Field, FieldValue}; let mut source = revision(); source.nodes.get_mut(&id(1)).unwrap().created = Some(7); let bytes = 134_333_468_649_123_456_u64.to_le_bytes(); - source.nodes.get_mut(&id(6)).unwrap().extra = vec![vec![Field { - id: 0x18001c65, - value: FieldValue::Bytes(&bytes), - }]]; + source.nodes.get_mut(&id(6)).unwrap().extra = vec![vec![ + Field { + id: 0x18001c65, + value: FieldValue::Bytes(&bytes), + }, + Field { + id: 0x1c001c30, + value: FieldValue::Bytes(&IDENTITY), + }, + ]]; source.nodes.get_mut(&id(1)).unwrap().children.clear(); source.nodes.get_mut(&id(1)).unwrap().structure.push(id(7)); let mut title = element(Kind::Title); @@ -1217,12 +1238,12 @@ mod tests { source.nodes.get_mut(&id(6)).unwrap().extra[0][0].value = FieldValue::Bytes(&bytes[..7]); assert!(Page::from_revision(&source, id(1)).is_err()); source.nodes.get_mut(&id(6)).unwrap().extra.clear(); - assert_eq!(Page::from_revision(&source, id(1)).unwrap().created, None); + let bare = Page::from_revision(&source, id(1)).unwrap(); + assert_eq!((bare.created, bare.identity), (None, None)); } #[test] fn title_role_and_minimum_width_come_from_root_properties() { - use crate::document::{Field, FieldValue}; let mut source = revision(); source.nodes.get_mut(&id(1)).unwrap().children.clear(); source.nodes.get_mut(&id(1)).unwrap().structure.push(id(7)); diff --git a/crates/onestore/tests/page_links.rs b/crates/onestore/tests/page_links.rs index 0aac0f7b91b586bf2a8d1c2b85a8bc2af6577afa..a0c3be26bfd9c6f0e66005ea13fd66c8d50358af 100644 --- a/crates/onestore/tests/page_links.rs +++ b/crates/onestore/tests/page_links.rs @@ -1,11 +1,18 @@ use onestore::{ ExGuid, PreparedEdit, RevisionIndex, Store, document::Document, - page::{Page, PageObject, PageParagraph, Paragraph, text::Edit}, + page::{ + Page, PageObject, PageParagraph, Paragraph, + link::{LinkTarget, internal_link}, + text::Edit, + }, }; const OUTLINES: &[u8] = include_bytes!("../../../corpus/outline-edit/before/notebook/synthetic.one"); +const NATIVE_LINKS: &[u8] = + include_bytes!("../../../corpus/link-edit/native-links/notebook/links.one"); +const NATIVE_BASE_PATH: &str = r"C:\one-tests\runs\capture\notebook\links.one"; const AUTHOR: &str = "Link author"; const CODE: &str = "\u{fddf}HYPERLINK \"https://example.invalid/rust\""; @@ -191,3 +198,144 @@ fn a_link_is_added_to_a_fresh_page_and_reads_back() { .unwrap(); } } + +fn field_codes(page: &mut Page) -> Vec { + body_paragraphs(page) + .iter() + .filter_map(|p| p.text()) + .filter_map(|t| { + let text = t.text.text(); + let start = text.find("\u{fddf}HYPERLINK \"")? + "\u{fddf}HYPERLINK \"".len(); + let end = start + text[start..].find('"')?; + Some(text[start..end].to_owned()) + }) + .collect() +} + +/// The URLs OneNote 2010 wrote for links to a page, a paragraph and the section +/// (`corpus/link-edit/native-links`) are what `internal_link` produces from the stored +/// identities: the section file identity, the target page's notebook-management identity +/// and the paragraph's stored identity. +#[test] +fn internal_links_match_what_onenote_stores() { + let section = Store::parse(NATIVE_LINKS).unwrap().header.file_id; + let (_, target) = page_by_title(NATIVE_LINKS, "Link target"); + let mut target_paragraphs = target.clone(); + // OneNote rewrote the target outline after linking; the link keeps the paragraph + // identity it saw then (n 28), whose page half is the current paragraph's. + let paragraph = ExGuid { + guid: body_paragraphs(&mut target_paragraphs)[0].id.guid, + n: 28, + }; + let (_, mut source) = page_by_title(NATIVE_LINKS, "Read about Rust the Rust site"); + let stored = field_codes(&mut source); + let page = LinkTarget::Page { + identity: target.identity.unwrap(), + title: &target.title, + }; + let object = LinkTarget::Object { + identity: target.identity.unwrap(), + title: &target.title, + object: paragraph, + }; + assert_eq!( + stored, + [ + "https://example.invalid/rust".to_owned(), + internal_link(section, NATIVE_BASE_PATH, page), + internal_link(section, NATIVE_BASE_PATH, object), + internal_link(section, NATIVE_BASE_PATH, LinkTarget::Section), + ] + ); + assert_eq!( + internal_link( + [0; 16], + "p", + LinkTarget::Page { + identity: [0; 16], + title: "A b/c" + } + ), + "onenote:#A%20b%2Fc§ion-id={00000000-0000-0000-0000-000000000000}&page-id={00000000-0000-0000-0000-000000000000}&end&base-path=p" + ); +} + +/// `ONESTORE_INTERNAL_LINK_EXPORT` names a new directory receiving the candidate for a cold +/// reopen: a section whose first page links to its second page and to a paragraph on it. +#[test] +fn a_page_links_to_another_page_and_its_paragraph() { + let source = onestore::create_section("links.one", "Linking page", "Author").unwrap(); + let creation = onestore::PageCreation::new(None, Some("Link target"), "Author").unwrap(); + let with_target = onestore::PreparedEdit::create_page(&source, &creation) + .unwrap() + .as_bytes() + .to_vec(); + let section = Store::parse(&with_target).unwrap().header.file_id; + let (_, target) = page_by_title(&with_target, "Link target"); + let (space, before) = page_by_title(&with_target, "Linking page"); + let paragraph = target + .objects + .iter() + .find_map(|object| match object { + PageObject::Outline(outline) if !outline.title => outline.paragraphs.first(), + _ => None, + }) + .map(|p| p.id); + let base_path = r"C:\one-tests\runs\capture\notebook\links.one"; + let page_url = internal_link( + section, + base_path, + LinkTarget::Page { + identity: target.identity.unwrap(), + title: &target.title, + }, + ); + let mut after = before.clone(); + let text = &mut body_paragraphs(&mut after)[0].text_mut().unwrap().text; + let end = text.utf16_offset(text.text().len()).unwrap(); + let base = text.format_at(end).unwrap().clone(); + let mut code = base.clone(); + code.hyperlink = Some(true); + code.hyperlink_label = Some(true); + code.hidden = Some(true); + let mut visible = base.clone(); + visible.hyperlink = Some(true); + visible.hyperlink_label = Some(true); + let mut runs = vec![ + (" ".to_owned(), base.clone()), + (format!("\u{fddf}HYPERLINK \"{page_url}\""), code.clone()), + ("Link target".to_owned(), visible.clone()), + ]; + if let Some(paragraph) = paragraph { + let object_url = internal_link( + section, + base_path, + LinkTarget::Object { + identity: target.identity.unwrap(), + title: &target.title, + object: paragraph, + }, + ); + runs.push((" ".to_owned(), base.clone())); + runs.push((format!("\u{fddf}HYPERLINK \"{object_url}\""), code)); + runs.push(("its paragraph".to_owned(), visible)); + } + text.apply(Edit { + range: end..end, + replacement: Paragraph::from_runs(runs), + }) + .unwrap(); + let written = PreparedEdit::page(&with_target, space, &after, AUTHOR).unwrap(); + assert_same(written.as_bytes(), space, &after); + if let Some(directory) = std::env::var_os("ONESTORE_INTERNAL_LINK_EXPORT") { + let directory = std::path::PathBuf::from(directory); + std::fs::create_dir(&directory).unwrap(); + std::fs::write(directory.join("links.one"), written.as_bytes()).unwrap(); + std::fs::write( + directory.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents("Open Notebook.onetoc2", &[("links.one", section)]) + .unwrap(), + ) + .unwrap(); + } +} diff --git a/crates/onestore/tests/page_tags.rs b/crates/onestore/tests/page_tags.rs index 9a31b1503e6c6cf64dc7763f07e0f45d7d42ec1b..16bd65cbd386c77452913718e578ed4a179a37d4 100644 --- a/crates/onestore/tests/page_tags.rs +++ b/crates/onestore/tests/page_tags.rs @@ -267,10 +267,18 @@ fn new_definitions_and_tags_publish_on_a_fresh_page() { let important = new_id().unwrap(); after.definitions.insert(task, definition("Rust task", 3)); let mut important_definition = definition("Important", 13); - let Kind::TagDefinition { action_type, .. } = &mut important_definition.kind else { + let Kind::TagDefinition { + action_type, + color, + highlight, + .. + } = &mut important_definition.kind + else { panic!() }; *action_type = Some(1); + *color = Some(0x0000_00ff); + *highlight = Some(0x0000_ffff); after.definitions.insert(important, important_definition); body_paragraphs(&mut after)[0] .text_mut() diff --git a/crates/snowbound/src/accessibility.rs b/crates/snowbound/src/accessibility.rs index 6185b2f576be29180026d3557eae913b1d18fd51..15d769b41cadcd7399c6528c44a83addba9632e2 100644 --- a/crates/snowbound/src/accessibility.rs +++ b/crates/snowbound/src/accessibility.rs @@ -611,6 +611,7 @@ mod tests { .snapshot(); title.title = true; let page = Page { + identity: None, title: "Header".into(), created: None, margin_origin: [36.0, 14.4], @@ -700,6 +701,7 @@ mod tests { use canvas::gpu::page::PageScene; use onestore::page::{Page, PageObject, Unsupported}; let page = Page { + identity: None, created: None, title: String::new(), margin_origin: [0.0; 2], diff --git a/crates/snowbound/src/main.rs b/crates/snowbound/src/main.rs index 7fd0920b7c91164c8ed28f071db70061d595c98d..1a5f81427e6d84d93609cb6878ddfb95b3fb333d 100644 --- a/crates/snowbound/src/main.rs +++ b/crates/snowbound/src/main.rs @@ -2126,6 +2126,7 @@ mod tests { }); fields[0].title = true; let page = Page { + identity: None, title: "Header".into(), created: Some(1), margin_origin: [36.0, 14.4], @@ -2229,6 +2230,7 @@ mod tests { ); } let page = onestore::page::Page { + identity: None, title: String::new(), created: None, margin_origin: [0.0; 2], @@ -2966,6 +2968,7 @@ mod tests { objects.reverse(); } let page = Page { + identity: None, created: None, title: String::new(), margin_origin: [0.0; 2], @@ -3143,6 +3146,7 @@ mod tests { ) .unwrap(); let page = Page { + identity: None, created: None, title: String::new(), margin_origin: [0.0; 2], diff --git a/tools/native/page-link.ps1 b/tools/native/page-link.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..8b4ab67977373c26c922fc7d13c01bb98939624e --- /dev/null +++ b/tools/native/page-link.ps1 @@ -0,0 +1,63 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' +$app = New-Object -ComObject OneNote.Application +$notebookId = '' +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebookId, 0) + $sectionId = '' + $app.OpenHierarchy('links.one', $notebookId, [ref]$sectionId, 0) + $deadline = [DateTime]::UtcNow.AddSeconds(30) + $page = $null + $pages = @() + do { + $hierarchy = '' + $app.GetHierarchy($sectionId, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + $pages = @($tree.SelectNodes('//*[local-name()="Page"]')) + if ($pages.Count -eq 1) { + $content = '' + $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1) + [xml]$page = $content + if (@($page.SelectNodes('//*[local-name()="OE"]')).Count -ge 1) { break } + } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if ($pages.Count -ne 1) { throw 'Expected one page in the section.' } + [IO.File]::WriteAllText((Join-Path $Root 'before.xml'), $content, [Text.Encoding]::UTF8) + $targetId = '' + $app.CreateNewPage($sectionId, [ref]$targetId, 0) + $target = '' + $app.UpdatePageContent($target, [DateTime]::MinValue, 1, $true) + $targetContent = '' + $app.GetPageContent($targetId, [ref]$targetContent, 3, 1) + [xml]$targetPage = $targetContent + $targetParagraph = $targetPage.SelectSingleNode('//*[local-name()="Outline"]/*[local-name()="OEChildren"]/*[local-name()="OE"]').GetAttribute('objectID') + $pageLink = '' + $app.GetHyperlinkToObject($targetId, '', [ref]$pageLink) + $paragraphLink = '' + $app.GetHyperlinkToObject($targetId, $targetParagraph, [ref]$paragraphLink) + $sectionLink = '' + $app.GetHyperlinkToObject($sectionId, '', [ref]$sectionLink) + @{ page = $pageLink; paragraph = $paragraphLink; section = $sectionLink; target = $targetId; target_paragraph = $targetParagraph } | ConvertTo-Json | Set-Content (Join-Path $Root 'links.json') -Encoding UTF8 + $children = $page.SelectSingleNode('//*[local-name()="Outline"]/*[local-name()="OEChildren"]') + foreach ($entry in @(@('Page link', $pageLink), @('Paragraph link', $paragraphLink), @('Section link', $sectionLink))) { + $oe = $page.CreateElement('one', 'OE', $namespace) + $t = $page.CreateElement('one', 'T', $namespace) + [void]$t.AppendChild($page.CreateCDataSection('' + $entry[0] + '')) + [void]$oe.AppendChild($t) + [void]$children.AppendChild($oe) + } + [IO.File]::WriteAllText((Join-Path $Root 'update.xml'), $page.OuterXml, [Text.Encoding]::UTF8) + $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false) + $app.SyncHierarchy($notebookId) + $app.CloseNotebook($notebookId, $false) + $notebookId = '' +} finally { + if ($notebookId) { $app.CloseNotebook($notebookId, $false) } + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + [GC]::Collect() + [GC]::WaitForPendingFinalizers() +} diff --git a/tools/test_link_edit.py b/tools/test_link_edit.py index 25fe7c74d3b716abc3688e1ef14f8a19ab86793a..1c4408d8d18d9201d6ae92db29f5156d9e1d1d7f 100644 --- a/tools/test_link_edit.py +++ b/tools/test_link_edit.py @@ -1,3 +1,5 @@ +import json +import re from pathlib import Path import runpy import shutil @@ -23,6 +25,30 @@ class LinkEditTest(unittest.TestCase): self.assertEqual([' '.join(text.split()) for text in texts], ['Read about Rust the Rust site']) + def test_onenote_links_between_pages_use_stored_identities(self): + with TemporaryDirectory() as temporary: + read = Path(temporary) / 'read' + shutil.copytree(FIXTURE / 'native-links/read', read) + compare(FIXTURE / 'native-links/notebook', read) + links = json.loads((FIXTURE / 'native-links/links.json').read_text(encoding='utf-8-sig')) + page, = (ET.parse(path).getroot() for path in sorted((FIXTURE / 'native-links/read').glob('page-*.xml')) + if ET.parse(path).getroot().get('name') == 'Read about Rust the Rust site') + 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)) + section = re.search(r'section-id=(\{[^}]*\})', links['page']).group(1) + page_id = re.search(r'page-id=(\{[^}]*\})', links['page']).group(1) + self.assertEqual(hrefs[1], 'onenote:#Link%%20target&section-id=%s&page-id=%s&end&base-path=C:\\one-tests\\runs\\capture\\notebook\\links.one' % (section, page_id)) + self.assertEqual(hrefs[3], 'onenote:#section-id=%s&end&base-path=C:\\one-tests\\runs\\capture\\notebook\\links.one' % section) + + def test_rust_internal_links_render_natively(self): + with TemporaryDirectory() as temporary: + read = Path(temporary) / 'read' + shutil.copytree(FIXTURE / 'internal/cold/read', read) + compare(FIXTURE / 'internal/candidate', read) + pages = {ET.parse(path).getroot().get('name'): ET.parse(path).getroot() for path in sorted((FIXTURE / 'internal/cold/read').glob('page-*.xml'))} + page = next(page for name, page in pages.items() if name.startswith('Linking page')) + 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) + self.assertRegex(text, r'^Linking page Link target$') + if __name__ == '__main__': unittest.main() diff --git a/tools/test_tag_edit.py b/tools/test_tag_edit.py index 9645227074f0d5e6f8a95906d2981cfa853ab83b..8ddb527d38ceefa8964330477356d0fbb30b51c0 100644 --- a/tools/test_tag_edit.py +++ b/tools/test_tag_edit.py @@ -20,8 +20,9 @@ class TagEditTest(unittest.TestCase): shutil.copytree(FIXTURE / 'cold/read', read) compare(FIXTURE / 'candidate', read) page, = (ET.parse(path).getroot() for path in sorted((FIXTURE / 'cold/read').glob('page-*.xml'))) - definitions = [(d.get('index'), d.get('type'), d.get('symbol'), d.get('name')) for d in page.findall('one:TagDef', ns)] - self.assertEqual(definitions, [('0', '0', '3', 'Rust task'), ('1', '1', '13', 'Important')]) + definitions = [(d.get('index'), d.get('type'), d.get('symbol'), d.get('name'), d.get('fontColor'), d.get('highlightColor')) + for d in page.findall('one:TagDef', ns)] + self.assertEqual(definitions, [('0', '0', '3', 'Rust task', 'automatic', 'none'), ('1', '1', '13', 'Important', '#FF0000', '#FFFF00')]) tagged = [([(t.get('index'), t.get('completed'), t.get('completionDate') is not None) for t in oe.findall('one:Tag', ns)], re.sub(r'<[^>]*>', '', oe.find('one:T', ns).text or '')) for oe in page.iter('{%s}OE' % ns['one']) if oe.find('one:T', ns) is not None] -- 2.54.0