| 1 | param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost, |
| 2 | [string]$CaptureOnly = '') |
| 3 | Set-StrictMode -Version Latest |
| 4 | $ErrorActionPreference = 'Stop' |
| 5 | if ($CaptureOnly) { |
| 6 | if ($CaptureOnly -notmatch '^[a-z0-9-]+$' -or $CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or |
| 7 | [Environment]::MachineName -ne $CloneHost -or |
| 8 | [IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($Root).TrimEnd('\')) -ne 'C:\one-tests\runs') { |
| 9 | throw 'Choose a named capture in the owned clone run.' |
| 10 | } |
| 11 | } else { |
| 12 | & "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost |
| 13 | } |
| 14 | $app = New-Object -ComObject OneNote.Application |
| 15 | $notebook = '' |
| 16 | $section = '' |
| 17 | $pages = [ordered]@{} |
| 18 | $namespace = 'http://schemas.microsoft.com/office/onenote/2010/onenote' |
| 19 | |
| 20 | function Save-Phase([string]$Name) { |
| 21 | $destination = Join-Path $Root $Name |
| 22 | [void](New-Item -ItemType Directory -Path $destination) |
| 23 | $deadline = [DateTime]::UtcNow.AddSeconds(60) |
| 24 | $previous = '' |
| 25 | $stableSince = [DateTime]::UtcNow |
| 26 | do { |
| 27 | $hierarchy = '' |
| 28 | $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) |
| 29 | [xml]$tree = $hierarchy |
| 30 | $contents = @() |
| 31 | foreach ($node in $tree.SelectNodes('//*[local-name()="Page"]')) { |
| 32 | $content = '' |
| 33 | $app.GetPageContent($node.GetAttribute('ID'), [ref]$content, 1, 1) |
| 34 | $contents += $content |
| 35 | } |
| 36 | $signature = $hierarchy + [String]::Join('|', $contents) |
| 37 | if ($signature -ne $previous) { |
| 38 | $previous = $signature |
| 39 | $stableSince = [DateTime]::UtcNow |
| 40 | } |
| 41 | if (([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { break } |
| 42 | if ([DateTime]::UtcNow -ge $deadline) { throw 'Native page content did not settle.' } |
| 43 | Start-Sleep -Milliseconds 250 |
| 44 | } while ($true) |
| 45 | [IO.File]::WriteAllText((Join-Path $destination 'hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) |
| 46 | for ($index = 0; $index -lt $contents.Count; $index++) { |
| 47 | [IO.File]::WriteAllText((Join-Path $destination ('page-{0:d3}.xml' -f $index)), $contents[$index], [Text.Encoding]::UTF8) |
| 48 | } |
| 49 | $app.SyncHierarchy($notebook) |
| 50 | $app.CloseNotebook($notebook, $false) |
| 51 | Copy-Item (Join-Path $Root 'notebook') (Join-Path $destination 'notebook') -Recurse |
| 52 | $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$script:notebook, 0) |
| 53 | $app.OpenHierarchy('Lifecycle.one', $notebook, [ref]$script:section, 0) |
| 54 | $beforePages = @($tree.SelectNodes('//*[local-name()="Page"]')) |
| 55 | $deadline = [DateTime]::UtcNow.AddSeconds(30) |
| 56 | do { |
| 57 | $reopened = '' |
| 58 | $app.GetHierarchy($section, 4, [ref]$reopened, 1) |
| 59 | [xml]$current = $reopened |
| 60 | $afterPages = @($current.SelectNodes('//*[local-name()="Page"]')) |
| 61 | if ($beforePages.Count -eq $afterPages.Count) { break } |
| 62 | Start-Sleep -Milliseconds 200 |
| 63 | } while ([DateTime]::UtcNow -lt $deadline) |
| 64 | [IO.File]::WriteAllText((Join-Path $destination 'reopened-hierarchy.xml'), $reopened, [Text.Encoding]::UTF8) |
| 65 | if ($beforePages.Count -ne $afterPages.Count) { throw 'Native reopen changed the page count.' } |
| 66 | for ($index = 0; $index -lt $beforePages.Count; $index++) { |
| 67 | foreach ($attribute in @('name', 'pageLevel')) { |
| 68 | if ($beforePages[$index].GetAttribute($attribute) -ne $afterPages[$index].GetAttribute($attribute)) { |
| 69 | throw 'Native reopen changed ordered page metadata.' |
| 70 | } |
| 71 | } |
| 72 | foreach ($key in @($pages.Keys)) { |
| 73 | if ($pages[$key] -eq $beforePages[$index].GetAttribute('ID')) { |
| 74 | $pages[$key] = $afterPages[$index].GetAttribute('ID') |
| 75 | } |
| 76 | } |
| 77 | } |
| 78 | if ($pages.Count) { $pages | ConvertTo-Json | Set-Content (Join-Path $destination 'pages.json') -Encoding UTF8 } |
| 79 | Write-Output $Name |
| 80 | } |
| 81 | |
| 82 | function Set-Title([string]$Page, [string]$Title) { |
| 83 | Write-Output ('Set title ' + $Page + ' to ' + $Title) |
| 84 | $value = [Security.SecurityElement]::Escape($Title) |
| 85 | $xml = '<one:Page xmlns:one="' + $namespace + '" ID="' + $Page + '"><one:Title><one:OE><one:T>' + $value + '</one:T></one:OE></one:Title></one:Page>' |
| 86 | $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false) |
| 87 | } |
| 88 | |
| 89 | function Set-Order([string[]]$Names, [int[]]$Levels) { |
| 90 | $xml = '<one:Section xmlns:one="' + $namespace + '" ID="' + $section + '">' |
| 91 | for ($index = 0; $index -lt $Names.Count; $index++) { |
| 92 | $xml += '<one:Page ID="' + $pages[$Names[$index]] + '" pageLevel="' + $Levels[$index] + '"/>' |
| 93 | } |
| 94 | $xml += '</one:Section>' |
| 95 | $app.UpdateHierarchy($xml, 1) |
| 96 | } |
| 97 | |
| 98 | try { |
| 99 | $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0) |
| 100 | $app.OpenHierarchy('Lifecycle.one', $notebook, [ref]$section, 3) |
| 101 | if ($CaptureOnly) { |
| 102 | Save-Phase $CaptureOnly |
| 103 | return |
| 104 | } |
| 105 | foreach ($style in 0..2) { |
| 106 | $page = '' |
| 107 | $app.CreateNewPage($section, [ref]$page, $style) |
| 108 | $pages['blank' + $style] = $page |
| 109 | } |
| 110 | Save-Phase '01-blank' |
| 111 | foreach ($name in @('parent', 'child', 'grandchild', 'duplicate', 'trailing', 'automatic')) { |
| 112 | $page = '' |
| 113 | $app.CreateNewPage($section, [ref]$page, 2) |
| 114 | $pages[$name] = $page |
| 115 | if ($name -ne 'automatic') { |
| 116 | Set-Title $page $(if ($name -in @('parent', 'duplicate')) { 'Same title' } else { $name }) |
| 117 | } |
| 118 | $body = [Security.SecurityElement]::Escape('Body ' + $name + ' ' + [char]::ConvertFromUtf32(0x1F980) + ' e' + [char]0x0301) |
| 119 | $xml = '<one:Page xmlns:one="' + $namespace + '" ID="' + $page + '"><one:Outline><one:OEChildren><one:OE><one:T><![CDATA[<b>' + $body + '</b> <i>preserved</i>]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page>' |
| 120 | $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false) |
| 121 | } |
| 122 | $pages | ConvertTo-Json | Set-Content (Join-Path $Root 'pages.json') -Encoding UTF8 |
| 123 | Save-Phase '02-authored' |
| 124 | Set-Title $pages['duplicate'] ('Renamed ' + [char]::ConvertFromUtf32(0x1F98B) + ' e' + [char]0x0301) |
| 125 | Set-Title $pages['trailing'] '' |
| 126 | Save-Phase '03-renamed' |
| 127 | Set-Order @('blank0','blank1','blank2','parent','child','grandchild','duplicate','trailing','automatic') @(1,1,1,1,2,3,1,1,1) |
| 128 | Save-Phase '04-nested' |
| 129 | Set-Order @('blank0','blank1','blank2','duplicate','parent','child','grandchild','trailing','automatic') @(1,1,1,1,1,2,3,1,1) |
| 130 | Save-Phase '05-reordered' |
| 131 | $app.DeleteHierarchy($pages['parent'], [DateTime]::MinValue, $false) |
| 132 | Save-Phase '06-deleted-parent' |
| 133 | } finally { |
| 134 | if ($notebook -and -not $CaptureOnly) { $app.CloseNotebook($notebook, $false) } |
| 135 | [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) |
| 136 | [GC]::Collect() |
| 137 | [GC]::WaitForPendingFinalizers() |
| 138 | } |