diff --git a/corpus/outline-edit/README.md b/corpus/outline-edit/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ceef168c198769c378a97ea093322fa2b6e8503f
--- /dev/null
+++ b/corpus/outline-edit/README.md
@@ -0,0 +1,40 @@
+# Native outline editing controls
+
+OneNote 2010 authored fourteen cases plus an unchanged source page. `before`,
+`after`, and `cold` retain notebook bytes and independent native XML. The final
+phase uses a fresh application cache. Cases preserve Unicode, hyperlinks, tags,
+nested descendants, siblings, and a separate outline.
+
+The controls cover leaf/subtree reordering, subtree indentation/outdentation,
+stored collapse/expansion, deletion of a leaf/subtree/sole paragraph/outline,
+outline position, fixed width, and automatic width. Keyboard subtree operations
+explicitly select descendants. Deleting the sole paragraph removes its empty
+outline. The native application sometimes advances unchanged text timestamps
+and adds `0x880034dd`; the test permits only those observed bookkeeping changes.
+
+COM outline replacement reallocates contained rich-text identities. Keyboard
+movement retains them. Native XML can reorder outlines by their positions while
+the file's page child order remains unchanged. Explicit width sets its native
+user-size flag; automatic width clears that flag. OneNote regenerates height
+from content instead of retaining the requested height. These observations
+constrain future typed operations; this corpus is not itself a writer feature.
+
+Persisted collapse values survive a cold open. Native keyboard collapse/expand
+can instead change only the client's cached view, so warm COM XML alone does
+not prove a stored-property edit. The controls here explicitly change the
+persisted default through COM.
+
+To regenerate, run `tools/native_runner.py` on
+`corpus/formatted-insertion/native-paragraph/candidate` with a new output directory,
+`--author tools/native/outline-editing.ps1 --expected-pages 15 --inspect
+--collect-notebook`. After inspection is ready, run
+`tools/native_outline_editing.py OUTPUT`. It freezes the before-image, records
+the actual keyboard/XML inputs, and requests final capture; the runner removes
+its clone. Move the returned `before-read` directory into `before/read`.
+Cold-open `OUTPUT/notebook` into another new output directory with the runner,
+`--expected-pages 15 --collect-notebook`. Public verification lives in
+`tools/test_outline_edit.py`.
+
+The controller selects the target again after activating the native window and
+transfers XML as UTF-8 data. Captured authoring scripts retain their exact source;
+identical immutable artifacts link to one canonical corpus copy.
diff --git a/corpus/outline-edit/actions/automatic-outline-size.ps1 b/corpus/outline-edit/actions/automatic-outline-size.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..153b5dee0670d47f41193fe7acb439e918e766b4
--- /dev/null
+++ b/corpus/outline-edit/actions/automatic-outline-size.ps1
@@ -0,0 +1,10 @@
+$ErrorActionPreference='Stop'
+$app=New-Object -ComObject OneNote.Application
+$xml = @'
+Anchor<span
+style='font-weight:bold' lang=en-US>Target 🦀 é</span><span lang=en-US> </span><a
+href="https://example.invalid/outline"><span lang=x-none>Link label</span></a><span
+style='font-style:italic'>Child</span>GrandchildTrailing sibling
+'@
+$app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
diff --git a/corpus/outline-edit/actions/automatic-outline-size.xml b/corpus/outline-edit/actions/automatic-outline-size.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6c8f562801f71cead33e06885e6777b3ded21998
--- /dev/null
+++ b/corpus/outline-edit/actions/automatic-outline-size.xml
@@ -0,0 +1,4 @@
+Anchor<span
+style='font-weight:bold' lang=en-US>Target 🦀 é</span><span lang=en-US> </span><a
+href="https://example.invalid/outline"><span lang=x-none>Link label</span></a><span
+style='font-style:italic'>Child</span>GrandchildTrailing sibling
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/collapse-subtree.ps1 b/corpus/outline-edit/actions/collapse-subtree.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..f680ea32ad296a77785f87430f91662a74d2414d
--- /dev/null
+++ b/corpus/outline-edit/actions/collapse-subtree.ps1
@@ -0,0 +1,10 @@
+$ErrorActionPreference='Stop'
+$app=New-Object -ComObject OneNote.Application
+$xml = @'
+Anchor<span
+style='font-weight:bold' lang=en-US>Target 🦀 é</span><span lang=en-US> </span><a
+href="https://example.invalid/outline"><span lang=x-none>Link label</span></a><span
+style='font-style:italic'>Child</span>GrandchildTrailing sibling
+'@
+$app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
diff --git a/corpus/outline-edit/actions/collapse-subtree.xml b/corpus/outline-edit/actions/collapse-subtree.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8e3f03e1f0189906c58e43fb4af175e723d130ea
--- /dev/null
+++ b/corpus/outline-edit/actions/collapse-subtree.xml
@@ -0,0 +1,4 @@
+Anchor<span
+style='font-weight:bold' lang=en-US>Target 🦀 é</span><span lang=en-US> </span><a
+href="https://example.invalid/outline"><span lang=x-none>Link label</span></a><span
+style='font-style:italic'>Child</span>GrandchildTrailing sibling
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/delete-leaf.ahk b/corpus/outline-edit/actions/delete-leaf.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..da634621c8c88e5e406d019818b6aef413c1bd46
--- /dev/null
+++ b/corpus/outline-edit/actions/delete-leaf.ahk
@@ -0,0 +1,15 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{BE6416CA-DCE5-4E1E-8258-1F4E46F2EE1F}{1}{B0}", "{54CC9973-5534-4978-B65D-E22FE3045B84}{45}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+app.NavigateTo("{BE6416CA-DCE5-4E1E-8258-1F4E46F2EE1F}{1}{B0}", "{54CC9973-5534-4978-B65D-E22FE3045B84}{45}{B0}", false)
+Sleep 300
+Send "{Left}^+{-}{Delete}"
+Sleep 300
diff --git a/corpus/outline-edit/actions/delete-leaf.json b/corpus/outline-edit/actions/delete-leaf.json
new file mode 100644
index 0000000000000000000000000000000000000000..e5965e0063547a11f248df7629ffb6fc13d4d916
--- /dev/null
+++ b/corpus/outline-edit/actions/delete-leaf.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Delete leaf - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/delete-leaf.png b/corpus/outline-edit/actions/delete-leaf.png
new file mode 100644
index 0000000000000000000000000000000000000000..b74fc60924b9f97c4ed1305b0b06ab024692f81e
Binary files /dev/null and b/corpus/outline-edit/actions/delete-leaf.png differ
diff --git a/corpus/outline-edit/actions/delete-only-paragraph.ahk b/corpus/outline-edit/actions/delete-only-paragraph.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..099f0f9db9a84feb6c4f28d1fe4f9929b116b86f
--- /dev/null
+++ b/corpus/outline-edit/actions/delete-only-paragraph.ahk
@@ -0,0 +1,15 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{2C90CD3D-EE7B-4FD1-9FE6-F67DE4BD37C2}{1}{B0}", "{40332BAB-F688-42D0-879E-15011BE0321C}{40}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+app.NavigateTo("{2C90CD3D-EE7B-4FD1-9FE6-F67DE4BD37C2}{1}{B0}", "{40332BAB-F688-42D0-879E-15011BE0321C}{40}{B0}", false)
+Sleep 300
+Send "{Left}^+{-}{Delete}"
+Sleep 300
diff --git a/corpus/outline-edit/actions/delete-only-paragraph.json b/corpus/outline-edit/actions/delete-only-paragraph.json
new file mode 100644
index 0000000000000000000000000000000000000000..01ee54bf02cc3d8356a066d8a938166de2586168
--- /dev/null
+++ b/corpus/outline-edit/actions/delete-only-paragraph.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Delete only paragraph - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/delete-only-paragraph.png b/corpus/outline-edit/actions/delete-only-paragraph.png
new file mode 100644
index 0000000000000000000000000000000000000000..953261f7dd45065d9e810c805b9ea311352a433a
Binary files /dev/null and b/corpus/outline-edit/actions/delete-only-paragraph.png differ
diff --git a/corpus/outline-edit/actions/delete-outline.ps1 b/corpus/outline-edit/actions/delete-outline.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..bb64da0da184baf1030aa2edae8fbf18671f62ca
--- /dev/null
+++ b/corpus/outline-edit/actions/delete-outline.ps1
@@ -0,0 +1,4 @@
+$ErrorActionPreference='Stop'
+$app=New-Object -ComObject OneNote.Application
+$app.DeletePageContent('{08B3F3DC-1FA5-4903-9EE6-7BD859999367}{1}{B0}', '{F0229B14-633D-47E1-B3F2-7B3AC62468DC}{39}{B0}', [DateTime]::MinValue, $false)
+[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
diff --git a/corpus/outline-edit/actions/delete-subtree.ahk b/corpus/outline-edit/actions/delete-subtree.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..9e51ff27fb527a3d0a2e4789228505830dac3751
--- /dev/null
+++ b/corpus/outline-edit/actions/delete-subtree.ahk
@@ -0,0 +1,15 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{03E12E10-4A4C-4C60-9643-7D516E223CD0}{1}{B0}", "{FBC38B3A-5810-4BD3-9608-0577DDB35904}{45}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+app.NavigateTo("{03E12E10-4A4C-4C60-9643-7D516E223CD0}{1}{B0}", "{FBC38B3A-5810-4BD3-9608-0577DDB35904}{45}{B0}", false)
+Sleep 300
+Send "{Left}^+{-}{Delete}"
+Sleep 300
diff --git a/corpus/outline-edit/actions/delete-subtree.json b/corpus/outline-edit/actions/delete-subtree.json
new file mode 100644
index 0000000000000000000000000000000000000000..12df043bd7033e17e4112c2891518a1fb7ba34ed
--- /dev/null
+++ b/corpus/outline-edit/actions/delete-subtree.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Delete subtree - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/delete-subtree.png b/corpus/outline-edit/actions/delete-subtree.png
new file mode 100644
index 0000000000000000000000000000000000000000..7890cf8b7378c571b78ba2d7cea390d4ec47d8c7
Binary files /dev/null and b/corpus/outline-edit/actions/delete-subtree.png differ
diff --git a/corpus/outline-edit/actions/expand-subtree.ps1 b/corpus/outline-edit/actions/expand-subtree.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..42eddecfcabaa8d18b7dd36696eba62f05b69875
--- /dev/null
+++ b/corpus/outline-edit/actions/expand-subtree.ps1
@@ -0,0 +1,10 @@
+$ErrorActionPreference='Stop'
+$app=New-Object -ComObject OneNote.Application
+$xml = @'
+Anchor<span
+style='font-weight:bold' lang=en-US>Target 🦀 é</span><span lang=en-US> </span><a
+href="https://example.invalid/outline"><span lang=x-none>Link label</span></a><span
+style='font-style:italic'>Child</span>GrandchildTrailing sibling
+'@
+$app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
diff --git a/corpus/outline-edit/actions/expand-subtree.xml b/corpus/outline-edit/actions/expand-subtree.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1a15e82c13ca862fab47e1f6f9eb463b6265a0ee
--- /dev/null
+++ b/corpus/outline-edit/actions/expand-subtree.xml
@@ -0,0 +1,4 @@
+Anchor<span
+style='font-weight:bold' lang=en-US>Target 🦀 é</span><span lang=en-US> </span><a
+href="https://example.invalid/outline"><span lang=x-none>Link label</span></a><span
+style='font-style:italic'>Child</span>GrandchildTrailing sibling
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/indent-subtree.ahk b/corpus/outline-edit/actions/indent-subtree.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..55f045da03e97cae8ffc74cbd484bf5cc8290b36
--- /dev/null
+++ b/corpus/outline-edit/actions/indent-subtree.ahk
@@ -0,0 +1,15 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{ED4F957C-5F6C-49AA-A0CD-A58F8B941BE8}{1}{B0}", "{D2D2C591-9007-4DF6-BD05-7F0BD6BDEFD9}{45}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+app.NavigateTo("{ED4F957C-5F6C-49AA-A0CD-A58F8B941BE8}{1}{B0}", "{D2D2C591-9007-4DF6-BD05-7F0BD6BDEFD9}{45}{B0}", false)
+Sleep 300
+Send "{Left}^+{-}!+{Right}"
+Sleep 300
diff --git a/corpus/outline-edit/actions/indent-subtree.json b/corpus/outline-edit/actions/indent-subtree.json
new file mode 100644
index 0000000000000000000000000000000000000000..cf760f50bc3a591cc184a46eb771a5f9bf746f1c
--- /dev/null
+++ b/corpus/outline-edit/actions/indent-subtree.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Indent subtree - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/indent-subtree.png b/corpus/outline-edit/actions/indent-subtree.png
new file mode 100644
index 0000000000000000000000000000000000000000..2ff2779cb8ac305916853536f04c6d1ebd686b6d
Binary files /dev/null and b/corpus/outline-edit/actions/indent-subtree.png differ
diff --git a/corpus/outline-edit/actions/move-leaf-down.ahk b/corpus/outline-edit/actions/move-leaf-down.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..0f4a3af0ce14f328c8347d267ab5aee33db621af
--- /dev/null
+++ b/corpus/outline-edit/actions/move-leaf-down.ahk
@@ -0,0 +1,15 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{3F7D85A7-2787-4DAA-B157-6E9FCFF94F28}{1}{B0}", "{8B6736A9-E8D2-47EA-BE99-3B0D9C0B147E}{45}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+app.NavigateTo("{3F7D85A7-2787-4DAA-B157-6E9FCFF94F28}{1}{B0}", "{8B6736A9-E8D2-47EA-BE99-3B0D9C0B147E}{45}{B0}", false)
+Sleep 300
+Send "{Left}^+{-}!+{Down}"
+Sleep 300
diff --git a/corpus/outline-edit/actions/move-leaf-down.json b/corpus/outline-edit/actions/move-leaf-down.json
new file mode 100644
index 0000000000000000000000000000000000000000..06ab27cafd9ee1d23e010f0faa032b8002e41e6c
--- /dev/null
+++ b/corpus/outline-edit/actions/move-leaf-down.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Move leaf down - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/move-leaf-down.png b/corpus/outline-edit/actions/move-leaf-down.png
new file mode 100644
index 0000000000000000000000000000000000000000..cdd9a81f83c0b6a5cc2bbb824a9131051b910afc
Binary files /dev/null and b/corpus/outline-edit/actions/move-leaf-down.png differ
diff --git a/corpus/outline-edit/actions/move-outline.ps1 b/corpus/outline-edit/actions/move-outline.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..c145f9f2989a227b7ebb195f9a6999f128de81db
--- /dev/null
+++ b/corpus/outline-edit/actions/move-outline.ps1
@@ -0,0 +1,10 @@
+$ErrorActionPreference='Stop'
+$app=New-Object -ComObject OneNote.Application
+$xml = @'
+Anchor<span
+style='font-weight:bold' lang=en-US>Target 🦀 é</span><span lang=en-US> </span><a
+href="https://example.invalid/outline"><span lang=x-none>Link label</span></a><span
+style='font-style:italic'>Child</span>GrandchildTrailing sibling
+'@
+$app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
diff --git a/corpus/outline-edit/actions/move-outline.xml b/corpus/outline-edit/actions/move-outline.xml
new file mode 100644
index 0000000000000000000000000000000000000000..51b93ba73f32436a02ee07542c7d76ed1dc8f3fa
--- /dev/null
+++ b/corpus/outline-edit/actions/move-outline.xml
@@ -0,0 +1,4 @@
+Anchor<span
+style='font-weight:bold' lang=en-US>Target 🦀 é</span><span lang=en-US> </span><a
+href="https://example.invalid/outline"><span lang=x-none>Link label</span></a><span
+style='font-style:italic'>Child</span>GrandchildTrailing sibling
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/move-subtree-down.ahk b/corpus/outline-edit/actions/move-subtree-down.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..0a43f0555a3e5d1e48a79fb9151bce844485e5db
--- /dev/null
+++ b/corpus/outline-edit/actions/move-subtree-down.ahk
@@ -0,0 +1,15 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{13DB2155-80B0-4789-8875-37E369FEB7C7}{1}{B0}", "{6FC1F2DD-27CB-41FF-88F4-6F22A1A31468}{45}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+app.NavigateTo("{13DB2155-80B0-4789-8875-37E369FEB7C7}{1}{B0}", "{6FC1F2DD-27CB-41FF-88F4-6F22A1A31468}{45}{B0}", false)
+Sleep 300
+Send "{Left}^+{-}!+{Down}"
+Sleep 300
diff --git a/corpus/outline-edit/actions/move-subtree-down.json b/corpus/outline-edit/actions/move-subtree-down.json
new file mode 100644
index 0000000000000000000000000000000000000000..97d228cbf80b69b57675d22bd7bf1917835bf125
--- /dev/null
+++ b/corpus/outline-edit/actions/move-subtree-down.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Move subtree down - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/move-subtree-down.png b/corpus/outline-edit/actions/move-subtree-down.png
new file mode 100644
index 0000000000000000000000000000000000000000..89906e21c38c38ef13cb79caca9b431830dcb137
Binary files /dev/null and b/corpus/outline-edit/actions/move-subtree-down.png differ
diff --git a/corpus/outline-edit/actions/move-subtree-up.ahk b/corpus/outline-edit/actions/move-subtree-up.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..13e16993ca539795080a48609392f30ac0459d7d
--- /dev/null
+++ b/corpus/outline-edit/actions/move-subtree-up.ahk
@@ -0,0 +1,15 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{FD63FFC3-A09F-42E1-BF61-EBD58FCFBACA}{1}{B0}", "{B71CF005-68C1-4007-8B1C-4ADC9BCA8D40}{45}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+app.NavigateTo("{FD63FFC3-A09F-42E1-BF61-EBD58FCFBACA}{1}{B0}", "{B71CF005-68C1-4007-8B1C-4ADC9BCA8D40}{45}{B0}", false)
+Sleep 300
+Send "{Left}^+{-}!+{Up}"
+Sleep 300
diff --git a/corpus/outline-edit/actions/move-subtree-up.json b/corpus/outline-edit/actions/move-subtree-up.json
new file mode 100644
index 0000000000000000000000000000000000000000..53108bfa4e4bc35edd8b496a9e63b3f98e2bb2fd
--- /dev/null
+++ b/corpus/outline-edit/actions/move-subtree-up.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Move subtree up - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/move-subtree-up.png b/corpus/outline-edit/actions/move-subtree-up.png
new file mode 100644
index 0000000000000000000000000000000000000000..d1eae54ac4021ae4cadaba9df74e8b5fe6976f37
Binary files /dev/null and b/corpus/outline-edit/actions/move-subtree-up.png differ
diff --git a/corpus/outline-edit/actions/outdent-subtree.ahk b/corpus/outline-edit/actions/outdent-subtree.ahk
new file mode 100644
index 0000000000000000000000000000000000000000..86b261e380f75d300ed42b46d4a4a57801f618da
--- /dev/null
+++ b/corpus/outline-edit/actions/outdent-subtree.ahk
@@ -0,0 +1,15 @@
+#Requires AutoHotkey v2.0
+OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))
+app := ComObject("OneNote.Application")
+app.NavigateTo("{0BDFFEA3-F8E9-4A45-B9E5-D249715CDC0B}{1}{B0}", "{92D67C94-716A-4489-AB52-B671E385FFA2}{45}{B0}", false)
+hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)
+if !hwnd
+ throw Error("OneNote window did not appear")
+WinMaximize(hwnd)
+WinActivate(hwnd)
+if !WinWaitActive(hwnd,, 10)
+ throw Error("OneNote did not become active")
+app.NavigateTo("{0BDFFEA3-F8E9-4A45-B9E5-D249715CDC0B}{1}{B0}", "{92D67C94-716A-4489-AB52-B671E385FFA2}{45}{B0}", false)
+Sleep 300
+Send "{Left}^+{-}!+{Left}"
+Sleep 300
diff --git a/corpus/outline-edit/actions/outdent-subtree.json b/corpus/outline-edit/actions/outdent-subtree.json
new file mode 100644
index 0000000000000000000000000000000000000000..124f98f311cbd539a19b49f5aef5add55e9683d9
--- /dev/null
+++ b/corpus/outline-edit/actions/outdent-subtree.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Outdent subtree - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/outline-edit/actions/outdent-subtree.png b/corpus/outline-edit/actions/outdent-subtree.png
new file mode 100644
index 0000000000000000000000000000000000000000..41e535243671659c91b57105262a453c1048655a
Binary files /dev/null and b/corpus/outline-edit/actions/outdent-subtree.png differ
diff --git a/corpus/outline-edit/actions/resize-outline.ps1 b/corpus/outline-edit/actions/resize-outline.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..bb2fd358b934cf12e5dc88cb8dc91e5608ac0d79
--- /dev/null
+++ b/corpus/outline-edit/actions/resize-outline.ps1
@@ -0,0 +1,10 @@
+$ErrorActionPreference='Stop'
+$app=New-Object -ComObject OneNote.Application
+$xml = @'
+Anchor<span
+style='font-weight:bold' lang=en-US>Target 🦀 é</span><span lang=en-US> </span><a
+href="https://example.invalid/outline"><span lang=x-none>Link label</span></a><span
+style='font-style:italic'>Child</span>GrandchildTrailing sibling
+'@
+$app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
diff --git a/corpus/outline-edit/actions/resize-outline.xml b/corpus/outline-edit/actions/resize-outline.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5ad8b1c061acaac82ab62a55a557259ebd109d60
--- /dev/null
+++ b/corpus/outline-edit/actions/resize-outline.xml
@@ -0,0 +1,4 @@
+Anchor<span
+style='font-weight:bold' lang=en-US>Target 🦀 é</span><span lang=en-US> </span><a
+href="https://example.invalid/outline"><span lang=x-none>Link label</span></a><span
+style='font-style:italic'>Child</span>GrandchildTrailing sibling
\ No newline at end of file
diff --git a/corpus/outline-edit/after/notebook/Open Notebook.onetoc2 b/corpus/outline-edit/after/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..5814b0f6d1941721eba9f84289aa7d970c3f3e23
Binary files /dev/null and b/corpus/outline-edit/after/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/outline-edit/after/notebook/synthetic.one b/corpus/outline-edit/after/notebook/synthetic.one
new file mode 100644
index 0000000000000000000000000000000000000000..1bdd1ba5f0594eaa9d5d09c35ba2abfc609abe75
Binary files /dev/null and b/corpus/outline-edit/after/notebook/synthetic.one differ
diff --git a/corpus/outline-edit/after/read/environment.json b/corpus/outline-edit/after/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..58ee419194981f8af330e4ba3acd8ddffac65e05
--- /dev/null
+++ b/corpus/outline-edit/after/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-EEF374A7",
+ "cold": false,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/outline-edit/after/read/hierarchy.xml b/corpus/outline-edit/after/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7dec3d4f59026fa7d84813aa4aca5296bef44bda
--- /dev/null
+++ b/corpus/outline-edit/after/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/after/read/page-000.xml b/corpus/outline-edit/after/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c4272ea11e8b1ec974de246988318c0708fbe69d
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/after/read/page-001.xml b/corpus/outline-edit/after/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bc2d7be8e659e8acf38b1a7d6416135fa4a316c2
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-001.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/after/read/page-002.xml b/corpus/outline-edit/after/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..abc986daedcca4817a3d6767c57f2b4326e0d7b7
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-002.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/after/read/page-003.xml b/corpus/outline-edit/after/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ebdeab24525adf63591870a907a35ab540f15352
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-003.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/after/read/page-004.xml b/corpus/outline-edit/after/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..34e77dfca7932948a786b7da4f591731faf6f3d1
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-004.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/after/read/page-005.xml b/corpus/outline-edit/after/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..036caf372813d5a7e946a082270c5c39fd58bf16
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/after/read/page-006.xml b/corpus/outline-edit/after/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..dce59bfa335bc42deb5b9084f7351721d17bcc99
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Target 🦀 é Link label]]>
diff --git a/corpus/outline-edit/after/read/page-007.xml b/corpus/outline-edit/after/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e1584490b2d2b754a72e819d119ae059328bd938
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-007.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/after/read/page-008.xml b/corpus/outline-edit/after/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e060da4a1c22da7160207826c677ae7665ead10a
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-008.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/after/read/page-009.xml b/corpus/outline-edit/after/read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ab1c72a605f8ac6c1b645e27b558cdc11bafde06
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-009.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/after/read/page-010.xml b/corpus/outline-edit/after/read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..32ec53f20d39b163e729e54c68e3649f1e96ee3a
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-010.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/after/read/page-011.xml b/corpus/outline-edit/after/read/page-011.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5c6bebfbdc45933b3b0e9e5cbe74c525c15282b0
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-011.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/after/read/page-012.xml b/corpus/outline-edit/after/read/page-012.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b3aedd9ea94ed1187f291d1d43ec4905ef7b389a
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-012.xml
@@ -0,0 +1,20 @@
+
+Bold 🦀 italic é color 東京
+End]]>Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/outline-edit/after/read/page-013.xml b/corpus/outline-edit/after/read/page-013.xml
new file mode 100644
index 0000000000000000000000000000000000000000..617b5ce187017395e7a767f53d1e23f3ec2a6537
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-013.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/after/read/page-014.xml b/corpus/outline-edit/after/read/page-014.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d5b7551ecafdd76dcd7606c86663456dd7daf030
--- /dev/null
+++ b/corpus/outline-edit/after/read/page-014.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/after/read/payloads.json b/corpus/outline-edit/after/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..ca0db3d0e4b60055164cc4d1938b3c0b1ab4e8e1
--- /dev/null
+++ b/corpus/outline-edit/after/read/payloads.json
@@ -0,0 +1 @@
+../../../paragraph-edit/before/read/payloads.json
\ No newline at end of file
diff --git a/corpus/outline-edit/before/notebook/Open Notebook.onetoc2 b/corpus/outline-edit/before/notebook/Open Notebook.onetoc2
new file mode 120000
index 0000000000000000000000000000000000000000..8f6ad22bd00746edfcf9c9d301bfe750a50fb3a3
--- /dev/null
+++ b/corpus/outline-edit/before/notebook/Open Notebook.onetoc2
@@ -0,0 +1 @@
+../../after/notebook/Open Notebook.onetoc2
\ No newline at end of file
diff --git a/corpus/outline-edit/before/notebook/synthetic.one b/corpus/outline-edit/before/notebook/synthetic.one
new file mode 100644
index 0000000000000000000000000000000000000000..5e79bebc16203aafa0c06942dd228774de6b84c5
Binary files /dev/null and b/corpus/outline-edit/before/notebook/synthetic.one differ
diff --git a/corpus/outline-edit/before/read/environment.json b/corpus/outline-edit/before/read/environment.json
new file mode 120000
index 0000000000000000000000000000000000000000..371d170656827612f03b625ef7c6b0ac98a799d7
--- /dev/null
+++ b/corpus/outline-edit/before/read/environment.json
@@ -0,0 +1 @@
+../../after/read/environment.json
\ No newline at end of file
diff --git a/corpus/outline-edit/before/read/hierarchy.xml b/corpus/outline-edit/before/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..e7ac991a0c3a3d8974149d4a264a0ce61b4448c9
--- /dev/null
+++ b/corpus/outline-edit/before/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/before/read/page-000.xml b/corpus/outline-edit/before/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7957fa8b0c6a7450673f8f9356c81b9108c7dc7a
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-000.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/before/read/page-001.xml b/corpus/outline-edit/before/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b908a8216c02783a80117e80dba2a98d63ae0a11
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-001.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/before/read/page-002.xml b/corpus/outline-edit/before/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..319ba7355660d79b56e40d5cc4bf1c53474a8fe6
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-002.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/before/read/page-003.xml b/corpus/outline-edit/before/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3da0b964cc63e253564a561e18803d42fa6ec835
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-003.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/before/read/page-004.xml b/corpus/outline-edit/before/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..29ec5712c43e6b54339682055df7a8f89305a92e
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-004.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/before/read/page-005.xml b/corpus/outline-edit/before/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..eeef7c1234b31762ab5b6a9f16c3a2fde9aca8be
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-005.xml
@@ -0,0 +1,4 @@
+
+Target 🦀 é Link label]]>
diff --git a/corpus/outline-edit/before/read/page-006.xml b/corpus/outline-edit/before/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c65e335d648dd8cde56af98bf338dd8287e1f8da
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-006.xml
@@ -0,0 +1,4 @@
+
+Target 🦀 é Link label]]>
diff --git a/corpus/outline-edit/before/read/page-007.xml b/corpus/outline-edit/before/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..beb4550b234d0f908db6b167f92c880106b073d6
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-007.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/before/read/page-008.xml b/corpus/outline-edit/before/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bf247aa34ad0e3cfb26624acdb619a6baa9d682e
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-008.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/before/read/page-009.xml b/corpus/outline-edit/before/read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ca36330befa4d006eff0014bbb2bdfc304ba8baa
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-009.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/before/read/page-010.xml b/corpus/outline-edit/before/read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..99f0afb26e4f560c00c66fb21acbf4724323fe36
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-010.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/before/read/page-011.xml b/corpus/outline-edit/before/read/page-011.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ecb7ab4aebcb6df55ac5a1122fceca963c567754
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-011.xml
@@ -0,0 +1,4 @@
+
+Target 🦀 é Link label]]>
diff --git a/corpus/outline-edit/before/read/page-012.xml b/corpus/outline-edit/before/read/page-012.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5b3d99e04e1a1e1846a8c0a4c7882976e86cb110
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-012.xml
@@ -0,0 +1,20 @@
+
+Bold 🦀 italic é color 東京
+End]]>Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/outline-edit/before/read/page-013.xml b/corpus/outline-edit/before/read/page-013.xml
new file mode 100644
index 0000000000000000000000000000000000000000..115b06cd05ecc9e9e16a055d3fd79e7f3ddfdeb3
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-013.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/before/read/page-014.xml b/corpus/outline-edit/before/read/page-014.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4110650d1127dfaee313b4b1df16aa1166eae98e
--- /dev/null
+++ b/corpus/outline-edit/before/read/page-014.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/before/read/payloads.json b/corpus/outline-edit/before/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..ca0db3d0e4b60055164cc4d1938b3c0b1ab4e8e1
--- /dev/null
+++ b/corpus/outline-edit/before/read/payloads.json
@@ -0,0 +1 @@
+../../../paragraph-edit/before/read/payloads.json
\ No newline at end of file
diff --git a/corpus/outline-edit/cases.json b/corpus/outline-edit/cases.json
new file mode 100644
index 0000000000000000000000000000000000000000..0a2dcee26a3d6d0ea4b84e60997a52327014d03d
--- /dev/null
+++ b/corpus/outline-edit/cases.json
@@ -0,0 +1,76 @@
+[
+ {
+ "name": "Move leaf down",
+ "keys": "{Left}^+{-}!+{Down}",
+ "leaf": true
+ },
+ {
+ "name": "Move subtree down",
+ "keys": "{Left}^+{-}!+{Down}"
+ },
+ {
+ "name": "Move subtree up",
+ "keys": "{Left}^+{-}!+{Up}"
+ },
+ {
+ "name": "Indent subtree",
+ "keys": "{Left}^+{-}!+{Right}"
+ },
+ {
+ "name": "Outdent subtree",
+ "keys": "{Left}^+{-}!+{Left}",
+ "nested": true
+ },
+ {
+ "collapse": true,
+ "name": "Collapse subtree"
+ },
+ {
+ "collapse": false,
+ "name": "Expand subtree",
+ "collapsed": true
+ },
+ {
+ "name": "Delete leaf",
+ "keys": "{Left}^+{-}{Delete}",
+ "leaf": true
+ },
+ {
+ "name": "Delete subtree",
+ "keys": "{Left}^+{-}{Delete}"
+ },
+ {
+ "only": true,
+ "name": "Delete only paragraph",
+ "keys": "{Left}^+{-}{Delete}",
+ "leaf": true
+ },
+ {
+ "name": "Delete outline",
+ "delete": "outline"
+ },
+ {
+ "name": "Move outline",
+ "position": {
+ "y": 216,
+ "x": 180
+ }
+ },
+ {
+ "name": "Resize outline",
+ "size": {
+ "isSetByUser": true,
+ "width": 144,
+ "height": 180
+ }
+ },
+ {
+ "fixed": true,
+ "name": "Automatic outline size",
+ "size": {
+ "isSetByUser": false,
+ "width": 360,
+ "height": 180
+ }
+ }
+]
diff --git a/corpus/outline-edit/cold/commands.jsonl b/corpus/outline-edit/cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..53d0785efaa962723658aff3e2559fbff80d63b4
--- /dev/null
+++ b/corpus/outline-edit/cold/commands.jsonl
@@ -0,0 +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-377CAF4B -ExpectedPages 15", "exit": 0, "stdout": "Read 1 sections and 15 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/outline-edit/cold/notebook/Open Notebook.onetoc2 b/corpus/outline-edit/cold/notebook/Open Notebook.onetoc2
new file mode 120000
index 0000000000000000000000000000000000000000..8f6ad22bd00746edfcf9c9d301bfe750a50fb3a3
--- /dev/null
+++ b/corpus/outline-edit/cold/notebook/Open Notebook.onetoc2
@@ -0,0 +1 @@
+../../after/notebook/Open Notebook.onetoc2
\ No newline at end of file
diff --git a/corpus/outline-edit/cold/notebook/synthetic.one b/corpus/outline-edit/cold/notebook/synthetic.one
new file mode 120000
index 0000000000000000000000000000000000000000..72fc135c56c45b43d59f89646c2fdeb67bb4a58a
--- /dev/null
+++ b/corpus/outline-edit/cold/notebook/synthetic.one
@@ -0,0 +1 @@
+../../after/notebook/synthetic.one
\ No newline at end of file
diff --git a/corpus/outline-edit/cold/read/environment.json b/corpus/outline-edit/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..4f8e94bbb659a4ff613f4eec375bb64b8d6f066e
--- /dev/null
+++ b/corpus/outline-edit/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-377CAF4B",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/outline-edit/cold/read/hierarchy.xml b/corpus/outline-edit/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..fcec25a54b16dac42f20f5c14b8406aad5a3f5cb
--- /dev/null
+++ b/corpus/outline-edit/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/cold/read/page-000.xml b/corpus/outline-edit/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c60f8ea3e8f26b24e37ddf2274553c9044758349
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-000.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/cold/read/page-001.xml b/corpus/outline-edit/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..65fb476b763ab71e8ee4b68e272456903939a5d6
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-001.xml
@@ -0,0 +1,20 @@
+
+Bold 🦀 italic é color 東京
+End]]>Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/outline-edit/cold/read/page-002.xml b/corpus/outline-edit/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1726d96c8da3e3b9dbaf037f0dd2164ad3a6cdda
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-002.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/cold/read/page-003.xml b/corpus/outline-edit/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ec0aad529930783c8c1798e40bf2f468fb6e3090
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/cold/read/page-004.xml b/corpus/outline-edit/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ced087f655ccefe073210d960c79e7d5d1182fdb
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-004.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/cold/read/page-005.xml b/corpus/outline-edit/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b79edbd8257cbd826c672e772099fbe84b56f745
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-005.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/cold/read/page-006.xml b/corpus/outline-edit/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6768e204645f56b63809ee91f1984222ad8f1e7a
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-006.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/cold/read/page-007.xml b/corpus/outline-edit/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cbd1cd2573bedfd97847c44c5b94367a68babf9b
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-007.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/cold/read/page-008.xml b/corpus/outline-edit/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ccb7cc0d300f229f25019dcdc0a0dbcb07ee9f13
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/cold/read/page-009.xml b/corpus/outline-edit/cold/read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c25290308638e39e21745bd26f0ea01faefc63a0
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-009.xml
@@ -0,0 +1,4 @@
+
+Target 🦀 é Link label]]>
diff --git a/corpus/outline-edit/cold/read/page-010.xml b/corpus/outline-edit/cold/read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5edf795b5c5fa9799e911e86a4a64d60bb38e5f2
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-010.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/cold/read/page-011.xml b/corpus/outline-edit/cold/read/page-011.xml
new file mode 100644
index 0000000000000000000000000000000000000000..6a745e8a141bbfcdb1892446dcd90e002c373c64
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-011.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/cold/read/page-012.xml b/corpus/outline-edit/cold/read/page-012.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b060ccd84bff0a2ae432eac2228567398a732fa4
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-012.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/cold/read/page-013.xml b/corpus/outline-edit/cold/read/page-013.xml
new file mode 100644
index 0000000000000000000000000000000000000000..d0fa9275a9c8c70f61be2e9014b61d8cfd2a28c7
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-013.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/outline-edit/cold/read/page-014.xml b/corpus/outline-edit/cold/read/page-014.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2815cc1b3e4ad22d141f4df2647103259d4e70f4
--- /dev/null
+++ b/corpus/outline-edit/cold/read/page-014.xml
@@ -0,0 +1,5 @@
+
+Target 🦀 é Link label]]>Child]]>
diff --git a/corpus/outline-edit/cold/read/payloads.json b/corpus/outline-edit/cold/read/payloads.json
new file mode 120000
index 0000000000000000000000000000000000000000..ca0db3d0e4b60055164cc4d1938b3c0b1ab4e8e1
--- /dev/null
+++ b/corpus/outline-edit/cold/read/payloads.json
@@ -0,0 +1 @@
+../../../paragraph-edit/before/read/payloads.json
\ No newline at end of file
diff --git a/corpus/outline-edit/cold/run.json b/corpus/outline-edit/cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..8049d48605d1cf1d140dc5ca311c085c9686527e
--- /dev/null
+++ b/corpus/outline-edit/cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/Users/clo/dev/one/evidence/m10/outline-native-05/notebook",
+ "expected_pages": 15,
+ "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/outline-edit/cold/scripts/cold.ps1 b/corpus/outline-edit/cold/scripts/cold.ps1
new file mode 120000
index 0000000000000000000000000000000000000000..2d413d263ed086517287ac23dc6715f760396d03
--- /dev/null
+++ b/corpus/outline-edit/cold/scripts/cold.ps1
@@ -0,0 +1 @@
+../../../paragraph-edit/reconciliation/keyboard/scripts/cold.ps1
\ No newline at end of file
diff --git a/corpus/outline-edit/cold/scripts/read.ps1 b/corpus/outline-edit/cold/scripts/read.ps1
new file mode 120000
index 0000000000000000000000000000000000000000..732e6bac15e039405c9d8cf16d0c283ffb554545
--- /dev/null
+++ b/corpus/outline-edit/cold/scripts/read.ps1
@@ -0,0 +1 @@
+../../../paragraph-edit/reconciliation/keyboard/scripts/read.ps1
\ No newline at end of file
diff --git a/corpus/outline-edit/cold/source.json b/corpus/outline-edit/cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..2e079881b956cfa57a36be05bed35da28f30deda
--- /dev/null
+++ b/corpus/outline-edit/cold/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4800,
+ "sha256": "341894b32b0433a1e8b647431f71e372d344c74e0f87699e45c9235964082551",
+ "mtime_ns": 1788890614579474155
+ },
+ {
+ "path": "synthetic.one",
+ "bytes": 134312,
+ "sha256": "2f63bdfcebe0afd3a0b72d26a189f979190795138d51ca5cc4bc3bd1c839d4ea",
+ "mtime_ns": 1788890614579649739
+ }
+]
diff --git a/corpus/outline-edit/cold/teardown.json b/corpus/outline-edit/cold/teardown.json
new file mode 120000
index 0000000000000000000000000000000000000000..2ff619474163b7ccced424b117d1e9371ef5be0e
--- /dev/null
+++ b/corpus/outline-edit/cold/teardown.json
@@ -0,0 +1 @@
+../../paragraph-edit/joined/teardown.json
\ No newline at end of file
diff --git a/corpus/outline-edit/commands.jsonl b/corpus/outline-edit/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..2af49502f1433d5f8088d94cf6040b490ffa0321
--- /dev/null
+++ b/corpus/outline-edit/commands.jsonl
@@ -0,0 +1,13 @@
+{"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-EEF374A7", "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-EEF374A7 -ExpectedPages 15 -UseCurrentCache -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 15 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}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\outline-action.ps1", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\outline-action.ps1", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\outline-action.ps1", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\outline-action.ps1", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\outline-action.ps1", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\outline-action.ps1", "exit": 0, "stdout": "", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -Command \"Rename-Item C:\\one-tests\\runs\\capture\\read before-read\"", "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-EEF374A7 -UseCurrentCache -ExpectedPages -1", "exit": 0, "stdout": "Read 1 sections and 15 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/outline-edit/inputs/Automatic outline size.xml b/corpus/outline-edit/inputs/Automatic outline size.xml
new file mode 100644
index 0000000000000000000000000000000000000000..96a6da9ab767d0cc10e5357374102895be411896
--- /dev/null
+++ b/corpus/outline-edit/inputs/Automatic outline size.xml
@@ -0,0 +1 @@
+Automatic outline sizeAnchorTarget 🦀 é Link label]]>Child]]>GrandchildTrailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Collapse subtree.xml b/corpus/outline-edit/inputs/Collapse subtree.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9bf63111820c0a698a921233e4f8ed14f1c34324
--- /dev/null
+++ b/corpus/outline-edit/inputs/Collapse subtree.xml
@@ -0,0 +1 @@
+Collapse subtreeAnchorTarget 🦀 é Link label]]>Child]]>GrandchildTrailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Delete leaf.xml b/corpus/outline-edit/inputs/Delete leaf.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f4a51bf491ea98982ffbd850a5c8bded2ade30ad
--- /dev/null
+++ b/corpus/outline-edit/inputs/Delete leaf.xml
@@ -0,0 +1 @@
+Delete leafAnchorTarget 🦀 é Link label]]>Trailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Delete only paragraph.xml b/corpus/outline-edit/inputs/Delete only paragraph.xml
new file mode 100644
index 0000000000000000000000000000000000000000..817bc1eff6c30fd7509770ddd1942a6b25f5731b
--- /dev/null
+++ b/corpus/outline-edit/inputs/Delete only paragraph.xml
@@ -0,0 +1 @@
+Delete only paragraphTarget 🦀 é Link label]]>Other outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Delete outline.xml b/corpus/outline-edit/inputs/Delete outline.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0d8621cb6abc0d460ade61b0f200d06b002aad3c
--- /dev/null
+++ b/corpus/outline-edit/inputs/Delete outline.xml
@@ -0,0 +1 @@
+Delete outlineAnchorTarget 🦀 é Link label]]>Child]]>GrandchildTrailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Delete subtree.xml b/corpus/outline-edit/inputs/Delete subtree.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0deef218f5905aafea5dcb14bc178eb3d7fd64ac
--- /dev/null
+++ b/corpus/outline-edit/inputs/Delete subtree.xml
@@ -0,0 +1 @@
+Delete subtreeAnchorTarget 🦀 é Link label]]>Child]]>GrandchildTrailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Expand subtree.xml b/corpus/outline-edit/inputs/Expand subtree.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bb0150c496fc014551523d313c4c0378443521ac
--- /dev/null
+++ b/corpus/outline-edit/inputs/Expand subtree.xml
@@ -0,0 +1 @@
+Expand subtreeAnchorTarget 🦀 é Link label]]>Child]]>GrandchildTrailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Indent subtree.xml b/corpus/outline-edit/inputs/Indent subtree.xml
new file mode 100644
index 0000000000000000000000000000000000000000..4768e92d52c21e5d6914250280764553bed89049
--- /dev/null
+++ b/corpus/outline-edit/inputs/Indent subtree.xml
@@ -0,0 +1 @@
+Indent subtreeAnchorTarget 🦀 é Link label]]>Child]]>GrandchildTrailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Move leaf down.xml b/corpus/outline-edit/inputs/Move leaf down.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c16ef23fe914c24ab4c2ca36727ca09f163c8dc9
--- /dev/null
+++ b/corpus/outline-edit/inputs/Move leaf down.xml
@@ -0,0 +1 @@
+Move leaf downAnchorTarget 🦀 é Link label]]>Trailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Move outline.xml b/corpus/outline-edit/inputs/Move outline.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7828025ceb30571b18e0b2720728c66a484cb8d7
--- /dev/null
+++ b/corpus/outline-edit/inputs/Move outline.xml
@@ -0,0 +1 @@
+Move outlineAnchorTarget 🦀 é Link label]]>Child]]>GrandchildTrailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Move subtree down.xml b/corpus/outline-edit/inputs/Move subtree down.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b6e4e8ebcd31426e75e6cee65439a1fe09b0aa0b
--- /dev/null
+++ b/corpus/outline-edit/inputs/Move subtree down.xml
@@ -0,0 +1 @@
+Move subtree downAnchorTarget 🦀 é Link label]]>Child]]>GrandchildTrailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Move subtree up.xml b/corpus/outline-edit/inputs/Move subtree up.xml
new file mode 100644
index 0000000000000000000000000000000000000000..51accf71a0420810aa929d67a7616dc2b18f51ab
--- /dev/null
+++ b/corpus/outline-edit/inputs/Move subtree up.xml
@@ -0,0 +1 @@
+Move subtree upAnchorTarget 🦀 é Link label]]>Child]]>GrandchildTrailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Outdent subtree.xml b/corpus/outline-edit/inputs/Outdent subtree.xml
new file mode 100644
index 0000000000000000000000000000000000000000..43836767ec3eeb161cc9914e8737e8f02a22deba
--- /dev/null
+++ b/corpus/outline-edit/inputs/Outdent subtree.xml
@@ -0,0 +1 @@
+Outdent subtreeAnchorTarget 🦀 é Link label]]>Child]]>GrandchildTrailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/inputs/Resize outline.xml b/corpus/outline-edit/inputs/Resize outline.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b7606ca17e311cda75c1a8f702e706218c7dc580
--- /dev/null
+++ b/corpus/outline-edit/inputs/Resize outline.xml
@@ -0,0 +1 @@
+Resize outlineAnchorTarget 🦀 é Link label]]>Child]]>GrandchildTrailing siblingOther outline
\ No newline at end of file
diff --git a/corpus/outline-edit/run.json b/corpus/outline-edit/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..ca219ce9c34add6adfebf41e39529f106822d54b
--- /dev/null
+++ b/corpus/outline-edit/run.json
@@ -0,0 +1,19 @@
+{
+ "notebook": "/Users/clo/dev/one/corpus/formatted-insertion/native-paragraph/candidate",
+ "expected_pages": 15,
+ "author": "tools/native/outline-editing.ps1",
+ "author_timeout_seconds": 600,
+ "inspect": true,
+ "collect_notebook": true,
+ "base": {
+ "file": "win7-office-base.qcow2",
+ "format": "qcow2",
+ "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
+ "virtual_size": 68719476736
+ },
+ "scripts": {
+ "author.ps1": "9bbbf862fcb0a5e7964ea3537c387a7cf7c62252b6c1a34c4a57a261f5cef297",
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/outline-edit/scripts/author.ps1 b/corpus/outline-edit/scripts/author.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..16ea3598a17de2cb9b32754489a14572e6091085
--- /dev/null
+++ b/corpus/outline-edit/scripts/author.ps1
@@ -0,0 +1,54 @@
+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
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('synthetic.one', $notebook, [ref]$section, 0)
+ $cases = @(
+ @{ name='Move leaf down'; keys='{Left}^+{-}!+{Down}'; leaf=$true },
+ @{ name='Move subtree down'; keys='{Left}^+{-}!+{Down}' },
+ @{ name='Move subtree up'; keys='{Left}^+{-}!+{Up}' },
+ @{ name='Indent subtree'; keys='{Left}^+{-}!+{Right}' },
+ @{ name='Outdent subtree'; keys='{Left}^+{-}!+{Left}'; nested=$true },
+ @{ name='Collapse subtree'; collapse=$true },
+ @{ name='Expand subtree'; collapse=$false; collapsed=$true },
+ @{ name='Delete leaf'; keys='{Left}^+{-}{Delete}'; leaf=$true },
+ @{ name='Delete subtree'; keys='{Left}^+{-}{Delete}' },
+ @{ name='Delete only paragraph'; keys='{Left}^+{-}{Delete}'; only=$true; leaf=$true },
+ @{ name='Delete outline'; delete='outline' },
+ @{ name='Move outline'; position=@{ x=180; y=216 } },
+ @{ name='Resize outline'; size=@{ width=144; height=180; isSetByUser=$true } },
+ @{ name='Automatic outline size'; size=@{ width=360; height=180; isSetByUser=$false }; fixed=$true }
+ )
+ $inputs = Join-Path $Root 'inputs'
+ [void](New-Item -ItemType Directory -Path $inputs)
+ foreach ($case in $cases) {
+ $page = ''
+ $app.CreateNewPage($section, [ref]$page, 0)
+ $label = 'Target ' + [char]::ConvertFromUtf32(0x1F980) + ' e' + [char]0x0301
+ $target = '' + $label + ' Link label]]>'
+ if (-not $case.ContainsKey('leaf')) {
+ $target += 'Child]]>Grandchild'
+ }
+ $target += ''
+ $body = if ($case.ContainsKey('only')) { $target } elseif ($case.ContainsKey('nested')) {
+ 'Anchor' + $target + 'Trailing sibling'
+ } else { 'Anchor' + $target + 'Trailing sibling' }
+ $size = if ($case.ContainsKey('fixed')) { '' } else { '' }
+ $xml = '' + $case.name + '' + $size + '' + $body + 'Other outline'
+ [IO.File]::WriteAllText((Join-Path $inputs ($case.name + '.xml')), $xml, [Text.Encoding]::UTF8)
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+ }
+ $cases | ConvertTo-Json -Depth 6 | Set-Content (Join-Path $Root 'cases.json') -Encoding UTF8
+ $app.SyncHierarchy($notebook)
+} finally {
+ if ($notebook) { $app.CloseNotebook($notebook, $false) }
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+}
diff --git a/corpus/outline-edit/scripts/cold.ps1 b/corpus/outline-edit/scripts/cold.ps1
new file mode 120000
index 0000000000000000000000000000000000000000..e724b69a78726b3117659c399ee21afe29b1c111
--- /dev/null
+++ b/corpus/outline-edit/scripts/cold.ps1
@@ -0,0 +1 @@
+../../paragraph-edit/reconciliation/keyboard/scripts/cold.ps1
\ No newline at end of file
diff --git a/corpus/outline-edit/scripts/read.ps1 b/corpus/outline-edit/scripts/read.ps1
new file mode 120000
index 0000000000000000000000000000000000000000..fd7b0eb1f71be9b47dfccd57f4a84f8100e5d7b7
--- /dev/null
+++ b/corpus/outline-edit/scripts/read.ps1
@@ -0,0 +1 @@
+../../paragraph-edit/reconciliation/keyboard/scripts/read.ps1
\ No newline at end of file
diff --git a/corpus/outline-edit/source.json b/corpus/outline-edit/source.json
new file mode 120000
index 0000000000000000000000000000000000000000..6e250e515f7a44d8c60e0f33e5026b759fad133b
--- /dev/null
+++ b/corpus/outline-edit/source.json
@@ -0,0 +1 @@
+../paragraph-edit/split/source.json
\ No newline at end of file
diff --git a/corpus/outline-edit/teardown.json b/corpus/outline-edit/teardown.json
new file mode 120000
index 0000000000000000000000000000000000000000..c9547c09998bb2980be603e91308c19703cb4d85
--- /dev/null
+++ b/corpus/outline-edit/teardown.json
@@ -0,0 +1 @@
+../paragraph-edit/joined/teardown.json
\ No newline at end of file
diff --git a/crates/onestore/src/document.rs b/crates/onestore/src/document.rs
index 95d2ac31a361fd45b8bb7e31a78f048ec0bad69e..73b09fec2798108a938c4f836395d5963d4e7528 100644
--- a/crates/onestore/src/document.rs
+++ b/crates/onestore/src/document.rs
@@ -273,7 +273,7 @@ pub enum Kind<'a> {
Paragraph {
lists: Vec,
paragraph_style: Option,
- /// Native saved defaults: 0 expanded, 1 collapsed; other values remain uninterpreted.
+ /// Saved state: 0 expanded, 1 collapsed, otherwise opaque; native UI overrides can stay cache-local.
collapse_state: Option,
},
OutlineGroup,
diff --git a/tools/native/outline-editing.ps1 b/tools/native/outline-editing.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..16ea3598a17de2cb9b32754489a14572e6091085
--- /dev/null
+++ b/tools/native/outline-editing.ps1
@@ -0,0 +1,54 @@
+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
+$app = New-Object -ComObject OneNote.Application
+$notebook = ''
+try {
+ $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0)
+ $section = ''
+ $app.OpenHierarchy('synthetic.one', $notebook, [ref]$section, 0)
+ $cases = @(
+ @{ name='Move leaf down'; keys='{Left}^+{-}!+{Down}'; leaf=$true },
+ @{ name='Move subtree down'; keys='{Left}^+{-}!+{Down}' },
+ @{ name='Move subtree up'; keys='{Left}^+{-}!+{Up}' },
+ @{ name='Indent subtree'; keys='{Left}^+{-}!+{Right}' },
+ @{ name='Outdent subtree'; keys='{Left}^+{-}!+{Left}'; nested=$true },
+ @{ name='Collapse subtree'; collapse=$true },
+ @{ name='Expand subtree'; collapse=$false; collapsed=$true },
+ @{ name='Delete leaf'; keys='{Left}^+{-}{Delete}'; leaf=$true },
+ @{ name='Delete subtree'; keys='{Left}^+{-}{Delete}' },
+ @{ name='Delete only paragraph'; keys='{Left}^+{-}{Delete}'; only=$true; leaf=$true },
+ @{ name='Delete outline'; delete='outline' },
+ @{ name='Move outline'; position=@{ x=180; y=216 } },
+ @{ name='Resize outline'; size=@{ width=144; height=180; isSetByUser=$true } },
+ @{ name='Automatic outline size'; size=@{ width=360; height=180; isSetByUser=$false }; fixed=$true }
+ )
+ $inputs = Join-Path $Root 'inputs'
+ [void](New-Item -ItemType Directory -Path $inputs)
+ foreach ($case in $cases) {
+ $page = ''
+ $app.CreateNewPage($section, [ref]$page, 0)
+ $label = 'Target ' + [char]::ConvertFromUtf32(0x1F980) + ' e' + [char]0x0301
+ $target = '' + $label + ' Link label]]>'
+ if (-not $case.ContainsKey('leaf')) {
+ $target += 'Child]]>Grandchild'
+ }
+ $target += ''
+ $body = if ($case.ContainsKey('only')) { $target } elseif ($case.ContainsKey('nested')) {
+ 'Anchor' + $target + 'Trailing sibling'
+ } else { 'Anchor' + $target + 'Trailing sibling' }
+ $size = if ($case.ContainsKey('fixed')) { '' } else { '' }
+ $xml = '' + $case.name + '' + $size + '' + $body + 'Other outline'
+ [IO.File]::WriteAllText((Join-Path $inputs ($case.name + '.xml')), $xml, [Text.Encoding]::UTF8)
+ $app.UpdatePageContent($xml, [DateTime]::MinValue, 1, $false)
+ }
+ $cases | ConvertTo-Json -Depth 6 | Set-Content (Join-Path $Root 'cases.json') -Encoding UTF8
+ $app.SyncHierarchy($notebook)
+} finally {
+ if ($notebook) { $app.CloseNotebook($notebook, $false) }
+ [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)
+ $app = $null
+ [GC]::Collect()
+ [GC]::WaitForPendingFinalizers()
+}
diff --git a/tools/native_outline_editing.py b/tools/native_outline_editing.py
new file mode 100644
index 0000000000000000000000000000000000000000..e3a38c9cf4ed300926e0b2e465df2804137174f7
--- /dev/null
+++ b/tools/native_outline_editing.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""Capture native outline controls during a disposable notebook inspection."""
+import argparse
+import base64
+import json
+from pathlib import Path
+import xml.etree.ElementTree as ET
+
+from native_runner import command, navigation_script, windows
+from native_xml import ns, Text
+
+
+def apply(output):
+ target = json.loads((output / 'machine.json').read_text())['name']
+ cases = json.loads((output / 'cases.json').read_text(encoding='utf-8-sig'))
+ pages = {page.get('name'): page for path in (output / 'read').glob('page-*.xml')
+ for page in [ET.parse(path).getroot()]}
+ before = output / 'before'
+ before.mkdir()
+ (output / 'notebook').rename(before / 'notebook')
+ actions = output / 'actions'
+ actions.mkdir()
+ for case in cases:
+ page = pages[case['name']]
+ outline = page.find('one:Outline', ns)
+ paragraph, = [node for node in outline.findall('.//one:OE', ns)
+ if ''.join(Text(node.find('one:T', ns).text or '').parts).startswith('Target ')]
+ page_id = page.get('ID')
+ object_id = (outline if case.get('delete') == 'outline' else paragraph).get('objectID')
+ stem = actions / case['name'].lower().replace(' ', '-')
+ if 'keys' in case:
+ script = navigation_script(page_id, object_id) + f'Send "{case["keys"]}"\nSleep 300\n'
+ stem.with_suffix('.ahk').write_text(script)
+ result = windows.do_exec(script, target=target, shot_delay_ms=500)
+ stem.with_suffix('.json').write_text(json.dumps({k: v for k, v in result.items() if k != 'png_b64'}, indent=2))
+ if result.get('png_b64'):
+ stem.with_suffix('.png').write_bytes(base64.b64decode(result['png_b64']))
+ if result.get('error') or result.get('exit') != 0 or not result.get('png_b64'):
+ raise RuntimeError(windows.text_result(result))
+ else:
+ if 'delete' in case:
+ page = ET.Element('delete', page=page_id, object=object_id)
+ operation = "$app.DeletePageContent($xml.DocumentElement.GetAttribute('page'), $xml.DocumentElement.GetAttribute('object'), [DateTime]::MinValue, $false)"
+ else:
+ for child in list(page):
+ if child.tag.rsplit('}', 1)[-1] in ('Outline', 'Title') and child is not outline:
+ page.remove(child)
+ if 'collapse' in case:
+ paragraph.set('collapsed', str(case['collapse']).lower())
+ for name in ('position', 'size'):
+ for key, value in case.get(name, {}).items():
+ outline.find('one:' + name.title(), ns).set(key, str(value).lower())
+ operation = "$app.UpdatePageContent($xml.OuterXml, [DateTime]::MinValue, 1, $false)"
+ ET.register_namespace('one', ns['one'])
+ payload = stem.with_suffix('.xml')
+ payload.write_bytes(ET.tostring(page, encoding='utf-8'))
+ result = windows.do_put(payload, r'C:\one-tests\outline-action.xml', target)
+ if result.get('error'):
+ raise RuntimeError(result['error'])
+ script = ("$ErrorActionPreference='Stop'\n[xml]$xml=[IO.File]::ReadAllText('C:\\one-tests\\outline-action.xml', [Text.Encoding]::UTF8)\n"
+ "$app=New-Object -ComObject OneNote.Application\n" + operation +
+ "\n[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app)\n")
+ source = stem.with_suffix('.ps1')
+ source.write_text(script, encoding='utf-8-sig')
+ result = windows.do_put(source, r'C:\one-tests\outline-action.ps1', target)
+ if result.get('error'):
+ raise RuntimeError(result['error'])
+ command(target, r'powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\one-tests\outline-action.ps1', output)
+ print(case['name'], flush=True)
+ (output / 'finish').touch()
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('output', type=Path)
+ apply(parser.parse_args().output)
diff --git a/tools/native_paragraph_reconciliation.py b/tools/native_paragraph_reconciliation.py
index 6b2569661f8e4b2316e4bdbe0a1ddd8714b4ba90..b48a9295e3f81d0e83eda76647d6cf281ea250ae 100644
--- a/tools/native_paragraph_reconciliation.py
+++ b/tools/native_paragraph_reconciliation.py
@@ -4,10 +4,9 @@ import argparse
import base64
import json
from pathlib import Path
-import re
import xml.etree.ElementTree as ET
-from native_runner import windows
+from native_runner import navigation_script, windows
from native_xml import ns
@@ -41,19 +40,8 @@ def apply(output):
steps = [(paragraphs[at].get('objectID'), keys)]
if join and change == 'prefix':
steps.append((paragraphs[1].get('objectID'), '{Right}Y'))
- script = ('#Requires AutoHotkey v2.0\n'
- 'OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))\n'
- 'app := ComObject("OneNote.Application")\n')
- for oid, keys in steps:
- for identifier in (page.get('ID'), oid):
- if not re.fullmatch(r'\{[0-9A-Fa-f-]+\}\{[0-9]+\}\{[0-9A-Fa-f]+\}', identifier):
- raise ValueError('Invalid native fixture identity')
- script += (f'app.NavigateTo("{page.get("ID")}", "{oid}", false)\n'
- 'hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)\n'
- 'if !hwnd\n throw Error("OneNote window did not appear")\n'
- 'WinMaximize(hwnd)\nWinActivate(hwnd)\n'
- 'if !WinWaitActive(hwnd,, 10)\n throw Error("OneNote did not become active")\n'
- f'Sleep 300\nSend "{keys}"\nSleep 300\n')
+ script = ''.join(navigation_script(page.get('ID'), oid) + f'Send "{keys}"\nSleep 300\n'
+ for oid, keys in steps)
stem = scripts / case['name'].lower().replace(' ', '-')
stem.with_suffix('.ahk').write_text(script)
result = windows.do_exec(script, target=target, shot_delay_ms=500)
diff --git a/tools/native_runner.py b/tools/native_runner.py
index f02c537bb1627e41c9b134db7ec3fdf8616080a7..2433cb12917812fa905511ee0c5878b994d41143 100644
--- a/tools/native_runner.py
+++ b/tools/native_runner.py
@@ -28,6 +28,21 @@ def command(target, text, output, timeout_ms=120000):
raise RuntimeError(windows.text_result(result))
+def navigation_script(page, object_id=''):
+ for identifier in [page] + ([object_id] if object_id else []):
+ if not re.fullmatch(r'\{[0-9A-Fa-f-]+\}\{[0-9]+\}\{[0-9A-Fa-f]+\}', identifier):
+ raise ValueError('The native identity cannot be used for navigation.')
+ return ('OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))\n'
+ 'DetectHiddenWindows false\napp := ComObject("OneNote.Application")\n'
+ f'app.NavigateTo("{page}", "{object_id}", false)\n'
+ 'hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)\n'
+ 'if !hwnd\n throw Error("OneNote window did not appear")\n'
+ 'WinMaximize(hwnd)\nWinActivate(hwnd)\n'
+ 'if !WinWaitActive(hwnd,, 10)\n throw Error("OneNote window did not become active")\n'
+ f'app.NavigateTo("{page}", "{object_id}", false)\n'
+ 'Sleep 300\n')
+
+
def install_agent(name, output):
source = output / 'agent.py'
source.write_bytes((ROOT / 'tools/w7/payload/agent.py').read_bytes())
@@ -170,16 +185,8 @@ def capture(notebook, output, expected_pages=-1, author=None, screenshots=False,
if screenshots:
for page in sorted((output / 'read').glob('page-*.xml')):
page_id = ET.parse(page).getroot().attrib['ID']
- if not re.fullmatch(r'\{[0-9A-Fa-f-]+\}\{[0-9]+\}\{[0-9A-Fa-f]+\}', page_id):
- raise ValueError('The native page identity cannot be used for navigation.')
result = windows.do_exec(
- 'OnError((exception, mode) => (FileAppend(exception.Message, "**"), ExitApp(1)))\n'
- 'DetectHiddenWindows false\napp := ComObject("OneNote.Application")\n'
- f'app.NavigateTo("{page_id}", "", false)\n'
- 'hwnd := WinWait("ahk_class Framework::CFrame ahk_exe ONENOTE.EXE",, 10)\n'
- 'if !hwnd\n throw Error("OneNote window did not appear")\n'
- 'WinMaximize(hwnd)\nWinActivate(hwnd)\n'
- 'if !WinWaitActive(hwnd,, 10)\n throw Error("OneNote window did not become active")\n',
+ navigation_script(page_id),
target=name, shot_delay_ms=1500)
page.with_suffix('.navigation.json').write_text(json.dumps({k: v for k, v in result.items() if k != 'png_b64'}, indent=2))
if result.get('png_b64'):
diff --git a/tools/test_outline_edit.py b/tools/test_outline_edit.py
new file mode 100644
index 0000000000000000000000000000000000000000..099a6dc346da3838c75b114adac285e4992d97a1
--- /dev/null
+++ b/tools/test_outline_edit.py
@@ -0,0 +1,131 @@
+import json
+from pathlib import Path
+import runpy
+import shutil
+import subprocess
+from tempfile import TemporaryDirectory
+import unittest
+import xml.etree.ElementTree as ET
+
+from document_model import EXPORTER, ordered_pages, walk
+from native_format import native_characters
+from native_xml import ns, Text
+
+ROOT = Path(__file__).resolve().parent.parent
+FIXTURE = ROOT / 'corpus/outline-edit'
+compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare']
+
+
+class OutlineEditTest(unittest.TestCase):
+ def preserved_node(self, old, new):
+ expected = dict(old)
+ if old['modified'] != new['modified']:
+ self.assertGreater(new['modified'], old['modified'])
+ expected['modified'] = new['modified']
+ if old['extra'] != new['extra']:
+ expected['extra'] = [old['extra'][0] + [{'id': 0x880034dd, 'value': 'NoData'}], *old['extra'][1:]]
+ self.assertEqual(new, expected)
+
+ def test_native_outline_controls_and_cold_reopen(self):
+ models, captures = {}, {}
+ with TemporaryDirectory() as temporary:
+ for phase in ('before', 'after', 'cold'):
+ folder = Path(temporary) / phase
+ shutil.copytree(FIXTURE / phase / 'read', folder / 'read')
+ compare(FIXTURE / phase / 'notebook', folder / 'read')
+ subprocess.run([EXPORTER, FIXTURE / phase / 'notebook/synthetic.one', folder / 'model'], check=True)
+ model = json.loads((folder / 'model/document.json').read_text())
+ models[phase] = {r['nodes'][r['roots']['2']]['kind']['title']: (r, page)
+ for _, _, r, page in ordered_pages(model)}
+ captures[phase] = {page.get('name'): page for path in (folder / 'read').glob('page-*.xml')
+ for page in [ET.parse(path).getroot()]}
+ self.assertEqual(len(models[phase]), 15)
+ cases = json.loads((FIXTURE / 'cases.json').read_text(encoding='utf-8-sig'))
+ self.assertEqual(len(cases), 14)
+ for case in cases:
+ name = case['name']
+ old, page = models['before'][name]
+ outline, other = [oid for oid in old['nodes'][page]['children'] if old['nodes'][oid]['kind']['type'] == 'Outline']
+ original = dict(walk(old, outline))
+ target, = [oid for oid, node in original.items() if node['kind']['type'] == 'Paragraph' and node['content']
+ and old['nodes'][node['content'][0]]['kind']['text'].startswith('Target ')]
+ parents = {child: oid for oid, node in original.items() for child in node['children']}
+ for phase in ('after', 'cold'):
+ with self.subTest(case=name, phase=phase):
+ new, current_page = models[phase][name]
+ self.assertEqual(page, current_page)
+ for oid, node in walk(old, other):
+ self.preserved_node(node, new['nodes'][oid])
+ graph = {oid: [list(node['children']), list(node['content'])] for oid, node in original.items()}
+ if name.startswith('Delete'):
+ selected = outline if name in ('Delete only paragraph', 'Delete outline') else target
+ for oid, _ in walk(old, selected):
+ del graph[oid]
+ if selected == outline:
+ self.assertNotIn(outline, new['nodes'][page]['children'])
+ active = {}
+ else:
+ graph[parents[selected]][0].remove(selected)
+ active = {oid: [node['children'], node['content']] for oid, node in walk(new, outline)}
+ self.assertEqual(active, graph)
+ elif 'keys' in case:
+ siblings = graph[outline][0]
+ if name in ('Move leaf down', 'Move subtree down'):
+ siblings[1], siblings[2] = siblings[2], siblings[1]
+ elif name == 'Move subtree up':
+ siblings[0], siblings[1] = siblings[1], siblings[0]
+ elif name == 'Indent subtree':
+ siblings.remove(target)
+ graph[siblings[0]][0] = [target]
+ elif name == 'Outdent subtree':
+ graph[parents[target]][0].remove(target)
+ siblings.insert(1, target)
+ else:
+ self.fail(name)
+ self.assertEqual({oid: [node['children'], node['content']] for oid, node in walk(new, outline)}, graph)
+ self.assertEqual(new['nodes'][target]['children'], old['nodes'][target]['children'])
+ for oid, node in original.items():
+ if node['kind']['type'] == 'RichText':
+ self.preserved_node(node, new['nodes'][oid])
+ else:
+ for oid, node in original.items():
+ if node['kind']['type'] == 'RichText':
+ continue
+ current = new['nodes'][oid]
+ self.assertEqual(current['children'], node['children'])
+ self.assertEqual(current['kind']['type'], node['kind']['type'])
+ for a, b in zip(node['content'], current['content'], strict=True):
+ self.assertEqual(old['nodes'][a]['kind']['text'], new['nodes'][b]['kind']['text'])
+ geometry = new['nodes'][outline]['layout']
+ for key, value in case.get('position', {}).items():
+ self.assertEqual(geometry[key], value)
+ if 'size' in case:
+ self.assertEqual(geometry['max_width'], case['size']['width'])
+ fields = {field['id'] for field in new['nodes'][outline]['extra'][0]}
+ self.assertEqual(0x88001cbd in fields, case['size']['isSetByUser'])
+ if 'collapse' in case:
+ self.assertEqual(bool(new['nodes'][target]['kind']['collapse_state']), case['collapse'])
+ node, = [node for node in captures[phase][name].findall('one:Outline//one:OE', ns)
+ if ''.join(Text(node.find('one:T', ns).text or '').parts).startswith('Target ')]
+ self.assertEqual(node.get('collapsed') in ('true', '1'), case['collapse'])
+ before_xml = captures['before'][name]
+ after_xml = captures[phase][name]
+ for left, right in zip(before_xml.findall('one:Outline', ns),
+ sorted(after_xml.findall('one:Outline', ns), key=lambda node: int(node.find('one:Position', ns).get('z'))), strict=True):
+ for a, b in zip(native_characters(before_xml, [left]), native_characters(after_xml, [right]), strict=True):
+ for (x, before), (y, after) in zip(a, b, strict=True):
+ self.assertEqual(x, y)
+ for key in before.keys() | after.keys():
+ default = 'automatic' if key in ('color', 'highlight') else False
+ self.assertEqual(before.get(key, default), after.get(key, default))
+ saved, saved_page = models['cold'][name]
+ current, current_page = models['after'][name]
+ self.assertEqual(current_page, saved_page)
+ self.assertEqual(current['nodes'][current_page]['children'], saved['nodes'][saved_page]['children'])
+ for outline in current['nodes'][current_page]['children']:
+ if current['nodes'][outline]['kind']['type'] != 'Outline':
+ continue
+ for oid, node in walk(current, outline):
+ for key in ('children', 'content', 'child_level'):
+ self.assertEqual(saved['nodes'][oid][key], node[key])
+ self.assertEqual(saved['nodes'][oid]['kind'].get('collapse_state'), node['kind'].get('collapse_state'))