diff --git a/arc/platforms.md b/arc/platforms.md
index 1acb4b564951137639b95bf4450ceb63fe97faa4..60ab0848199c82f66a7c3e2a7ba8e80145b410f2 100644
--- a/arc/platforms.md
+++ b/arc/platforms.md
@@ -29,8 +29,13 @@ keyboard, the toolbar and the macOS menu bar all run commands from it.
### macOS
-- The title bar is transparent. The app draws its own around AppKit's traffic
- lights, as part of the same frame as the rest of the chrome. Under the whole
+- The toolbar's row is the title bar. An empty `NSToolbar`, unified compact
+ from macOS 11, with the title hidden makes the title bar the row's height,
+ and AppKit places the traffic lights in it; a press in the row's gaps drags
+ the window. The title is still set, for the Window menu, Mission Control and
+ VoiceOver. `--screenshot` paints the lights where the hidden window's AppKit
+ put them. The app draws the row as part of the same frame as the rest of the
+ chrome. Under the whole
window lies AppKit's title bar material, an `NSVisualEffectView`, and the
chrome is drawn transparent over it, so the title bar, toolbar, tab row and
sidebar take the system's desktop tint, appearance and focus state exactly.
@@ -52,9 +57,14 @@ keyboard, the toolbar and the macOS menu bar all run commands from it.
### Linux
- X11 and Wayland. Title bars come from the window manager, or on Wayland from
- the compositor where it offers server-side decorations and a client-side
- frame otherwise. The app draws its own window controls only where no frame
- could be made.
+ the compositor where it offers server-side decorations, and the toolbar's
+ row lies beneath them, as in Dolphin and Kate. Elsewhere on Wayland, as on
+ GNOME, the row is the title bar, as a GTK 4 header bar holding tools: winit's
+ Adwaita frame keeps its shadow, corners and resize edges but has no header,
+ and the row ends in the window's buttons in the order GNOME's `button-layout`
+ lists them. Under that frame on GNOME the window erases its bottom
+ corners to transparent pixels and the frame draws libadwaita's window edge
+ round it: radius, shadows and outline. KWin rounds Breeze's corners itself.
- The toolbar and the rest of the chrome continue the title bar's fill,
focused and not: on KDE the colour scheme's header colours from
`kdeglobals`, as KWin paints its title bars, and on GNOME winit's Adwaita
diff --git a/arc/ui.md b/arc/ui.md
index a553d585df2a643383f6986f94e4f912685efe58..4287b45b2d1028c9ef760e7538ed0b517c7a739d 100644
--- a/arc/ui.md
+++ b/arc/ui.md
@@ -23,9 +23,10 @@ quickly and never feel like they're waiting on an animation. Animated values
ease exponentially toward their targets with a short half-life. That is
frame-rate independent, a retargeted animation continues smoothly from where
it is, and it settles fast. Popups open and close on short timed curves,
-slow enough to follow: a menu grows out of the pointer or its button as it
+slow enough to follow: a menu swings out of the pointer or its button as it
fades in, a combo's field widens into its list, and a dialog swings up into
-place over a dimmed window, as Windows opens a window. On GNOME and KDE, menus instead
+place over a dimmed window, as Windows opens a window; the command palette swings
+in the same way near the window's top. On GNOME and KDE, menus instead
look and move as the desktop's own (see [platforms](platforms.md)).
## Why not native widgets
@@ -62,7 +63,7 @@ frame N
route input ─── against frame N-1's layout (hover, press, focus, drags, wheel)
build ───────── app code declares boxes; each reads its Signal (clicked, dragging, events…)
solve layout ── per axis: pixels · label size · fraction of an ancestor · sum of children
- overflow is shared out by each box's strictness
+ overflow taken from space, then by folding groups, then by strictness
paint ───────── Layers: primitives under a clip, or a Custom box the host paints
after paint ─── work the frame asked for (page requests, saving) runs, then asks for a frame
```
@@ -75,8 +76,17 @@ frame N
it is being declared. It sounds odd and is invisible in practice.
- **Layout is solved after building**, per axis. A box can be sized in fixed
pixels, by its label, as a fraction of an ancestor, or by its children. When
- siblings overflow, each gives up space according to its *strictness*. That
- one knob covers most of what flexbox is usually needed for.
+ siblings overflow, space sized from an ancestor gives way first. Then a row
+ *folds* its groups, boxes with a full and a folded form (`Spec::fold`), by
+ priority. Only then do boxes give up room by their *strictness*, the least
+ strict first. That one knob covers most of what flexbox is usually needed
+ for.
+- **The toolbar is one row that never overflows.** Both forms of every group
+ are built each frame and the solver picks, so a group folds in the frame the
+ window narrows, with no widths remembered or worked out by the app. A folded
+ group is a dropdown of the same command-table rows, and a gallery it lists
+ opens under it, since a folded-away box takes its group's rectangle. The
+ section tabs scroll sideways past the row's room, fading out where cut.
- **Nothing runs while nothing changes.** The kit reports whether it wants
another frame (queued input, an animation still settling) and when a timed
change such as a caret blink is due. The app sleeps in between.
@@ -113,6 +123,11 @@ rounded or gradient rectangles, pen strokes and raster images.
ordinary ones.
- **Batches** merge consecutive primitives that share a texture and clip,
without reordering paint.
+- **A popup in motion is one picture.** Layers sharing a motion draw
+ offscreen together, then fade and lean back as a whole, so nothing beneath
+ or within shows through a row. A popup's contents clip to its rounded
+ outline, and one opening over a combo lays its rows out at their final
+ width while only the outline widens.
- **Caches are bounded.** The glyph atlas and image cache have fixed budgets.
Eviction keeps everything the current frame needs and rebuilds after
pressure. Images are filtered in linear light with premultiplied alpha.
diff --git a/corpus/page-background/README.md b/corpus/page-background/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ebf4088f7a77eee2effcad6dea6e302366d59088
--- /dev/null
+++ b/corpus/page-background/README.md
@@ -0,0 +1,13 @@
+# Page background
+
+`candidate` is `backgrounds_go_on_and_off_pages_that_have_content` in
+`crates/snowbound/src/manage.rs` (`SNOWBOUND_BACKGROUND_EXPORT`), on a copy of
+`rule-lines/native/notebook`: the Page Color menu's ops give the ruled "Standard" page
+Ivy's art (`manage::art_ops`), give "Wide" the art and take it away again, and colour
+"SmallGrid" `0x00c8d8e8`, a COLORREF outside OneNote's menu.
+
+`cold` is its fresh OneNote 2010 read: "Standard" keeps its rule lines under one
+background image (`backgroundImage="true"`), "Wide" keeps its lines and no image, and
+"SmallGrid" reads back `color="#E8D8C8"` and shows it under its grid. `read/page-006.png`
+shows OneNote drawing the art over the rule lines. `tools/test_page_background.py`
+checks this without a VM.
diff --git a/corpus/page-background/candidate/Open Notebook.onetoc2 b/corpus/page-background/candidate/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..2d2785048e719aaea688d47f540628bf86baaf99
Binary files /dev/null and b/corpus/page-background/candidate/Open Notebook.onetoc2 differ
diff --git a/corpus/page-background/candidate/Rules.one b/corpus/page-background/candidate/Rules.one
new file mode 100644
index 0000000000000000000000000000000000000000..b3cac5439567e52d91afb41e0aea8bfe756f673d
Binary files /dev/null and b/corpus/page-background/candidate/Rules.one differ
diff --git a/corpus/page-background/cold/commands.jsonl b/corpus/page-background/cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..078755456d9b165dd24331a220e48eef3cdfdbea
--- /dev/null
+++ b/corpus/page-background/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-5EF70348 -ExpectedPages 11 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 11 pages.\r\n", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\read -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
diff --git a/corpus/page-background/cold/machine.json b/corpus/page-background/cold/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..e58b873f2d473eca43130194d68fecdd79deb49b
--- /dev/null
+++ b/corpus/page-background/cold/machine.json
@@ -0,0 +1 @@
+{"name": "m6-5ef70348", "hostname": "ONE-M6-5EF70348"}
diff --git a/corpus/page-background/cold/read/environment.json b/corpus/page-background/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..fccbb69c546565860b9d41302c324d6405a6f1fd
--- /dev/null
+++ b/corpus/page-background/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-5EF70348",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/page-background/cold/read/hierarchy.xml b/corpus/page-background/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2480abbaf9351670fbea32ca881eda9ec2b47ce7
--- /dev/null
+++ b/corpus/page-background/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-background/cold/read/page-000.navigation.json b/corpus/page-background/cold/read/page-000.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..7824d62a06fb14853ccd8fca71946931cbe8e9ec
--- /dev/null
+++ b/corpus/page-background/cold/read/page-000.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "None - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/page-background/cold/read/page-000.png b/corpus/page-background/cold/read/page-000.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d53a16941a97c24ef8e89388659739401287191
Binary files /dev/null and b/corpus/page-background/cold/read/page-000.png differ
diff --git a/corpus/page-background/cold/read/page-000.xml b/corpus/page-background/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..12cf428804b54db3214d4d440d9a26a08090c293
--- /dev/null
+++ b/corpus/page-background/cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-background/cold/read/page-001.navigation.json b/corpus/page-background/cold/read/page-001.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..1d051ab18621c4450d424efbbd3056b504248b1b
--- /dev/null
+++ b/corpus/page-background/cold/read/page-001.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "College - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/page-background/cold/read/page-001.png b/corpus/page-background/cold/read/page-001.png
new file mode 100644
index 0000000000000000000000000000000000000000..86f49f9425b6b98a8ed88aa8c1675759632059e3
Binary files /dev/null and b/corpus/page-background/cold/read/page-001.png differ
diff --git a/corpus/page-background/cold/read/page-001.xml b/corpus/page-background/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3578db1b2139354625912e9473a87ebbed2eff28
--- /dev/null
+++ b/corpus/page-background/cold/read/page-001.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-background/cold/read/page-002.navigation.json b/corpus/page-background/cold/read/page-002.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..909744712d947f9080464d99bb701cd7de4071c8
--- /dev/null
+++ b/corpus/page-background/cold/read/page-002.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "VeryLargeGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/page-background/cold/read/page-002.png b/corpus/page-background/cold/read/page-002.png
new file mode 100644
index 0000000000000000000000000000000000000000..75a53d42b8eb63e08cdc6d28336d940273b7e2f4
Binary files /dev/null and b/corpus/page-background/cold/read/page-002.png differ
diff --git a/corpus/page-background/cold/read/page-002.xml b/corpus/page-background/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7b7b9f465790ce5737d5f211006ff058494757be
--- /dev/null
+++ b/corpus/page-background/cold/read/page-002.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-background/cold/read/page-003.navigation.json b/corpus/page-background/cold/read/page-003.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..7ccafd7fd59c95368f51d575f411c3fb815d3863
--- /dev/null
+++ b/corpus/page-background/cold/read/page-003.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "MediumGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/page-background/cold/read/page-003.png b/corpus/page-background/cold/read/page-003.png
new file mode 100644
index 0000000000000000000000000000000000000000..4adfda1220e6a91319553962aa0b842b9c535abe
Binary files /dev/null and b/corpus/page-background/cold/read/page-003.png differ
diff --git a/corpus/page-background/cold/read/page-003.xml b/corpus/page-background/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3544c37d82f5ce4bbe4d70bdc5ac292060eed29b
--- /dev/null
+++ b/corpus/page-background/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-background/cold/read/page-004.navigation.json b/corpus/page-background/cold/read/page-004.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..007050b6d7a6917406f9ab04289d337fab5d8d43
--- /dev/null
+++ b/corpus/page-background/cold/read/page-004.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Wide - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/page-background/cold/read/page-004.png b/corpus/page-background/cold/read/page-004.png
new file mode 100644
index 0000000000000000000000000000000000000000..d70d67d33fff88c810540c9cc6048c0da36b0d47
Binary files /dev/null and b/corpus/page-background/cold/read/page-004.png differ
diff --git a/corpus/page-background/cold/read/page-004.xml b/corpus/page-background/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cd5eb9a8d6fd489b97c26548fb5c08c58b3d5e71
--- /dev/null
+++ b/corpus/page-background/cold/read/page-004.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-background/cold/read/page-005.navigation.json b/corpus/page-background/cold/read/page-005.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..7f25097d7917248dd57c3d10c5466fadaaa84a95
--- /dev/null
+++ b/corpus/page-background/cold/read/page-005.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Narrow - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/page-background/cold/read/page-005.png b/corpus/page-background/cold/read/page-005.png
new file mode 100644
index 0000000000000000000000000000000000000000..0ff75c9f29678987fa5562d0c36b6d9438c63975
Binary files /dev/null and b/corpus/page-background/cold/read/page-005.png differ
diff --git a/corpus/page-background/cold/read/page-005.xml b/corpus/page-background/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a50c40d773c0bc67794d26ec2a6f57a39c985c87
--- /dev/null
+++ b/corpus/page-background/cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-background/cold/read/page-006.navigation.json b/corpus/page-background/cold/read/page-006.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..74f74505c909f96ff84679b418cd721b7be7459f
--- /dev/null
+++ b/corpus/page-background/cold/read/page-006.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Standard - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/page-background/cold/read/page-006.png b/corpus/page-background/cold/read/page-006.png
new file mode 100644
index 0000000000000000000000000000000000000000..b529348ff769e5ea373a48ffec8425339c59d681
Binary files /dev/null and b/corpus/page-background/cold/read/page-006.png differ
diff --git a/corpus/page-background/cold/read/page-006.xml b/corpus/page-background/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3d7a8ca5f93aef9d28145f04b73701ba123c7855
--- /dev/null
+++ b/corpus/page-background/cold/read/page-006.xml
@@ -0,0 +1,353 @@
+
+/9j/4AAQSkZJRgABAgAAAQABAAD//gAYU25vd2JvdW5kIHRlbXBsYXRlIGl2ef/AABEIAxgA2AMB
+EQACEQEDEQH/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsL
+EBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQU
+FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQID
+BAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0BAgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHB
+FVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2
+d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna
+4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEBAQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1
+EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ0
+4SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeI
+iYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery
+8/T19vf4+fr/2gAMAwEAAhEDEQA/AP1ToAKAOS8f+KbvwbFYamkX2nThL5V3EB8wDfdcH2wRjoc1
+5GYYqpg+SqleN7NfkzGtV9jDnauup0el6nbazp8F7aSCW2nUOjjuK9KlVhXgqkHdM1TUldbHK/Cf
+/kW7r/sIXH/odeblf8F/4pfmcGB/hv1Z2leuegFABQAUAFABQAUAFABQAUAFABQAUAFABQAUAFAB
+QAUAFABQAUAFABQBW1HTrbVrGazu4lntpl2PG3QisqtKFaDpzV0xNKSszmNP0uP4X+ENUZLh7y1t
+vMuIUkGCgxwme/PfjrXlU6X9l4ao+bmSu1/l95lCEcPTstlf/Mz/AIM3Dt4GM03+ta6mZsjHzFuf
+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
diff --git a/corpus/page-background/cold/read/page-007.navigation.json b/corpus/page-background/cold/read/page-007.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..6ea1c2e7cfb4aec222874c087cbbaf51b80cb3c7
--- /dev/null
+++ b/corpus/page-background/cold/read/page-007.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "LargeGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/page-background/cold/read/page-007.png b/corpus/page-background/cold/read/page-007.png
new file mode 100644
index 0000000000000000000000000000000000000000..16bcfdc5106ef59235784c52de70b3b2e78111cb
Binary files /dev/null and b/corpus/page-background/cold/read/page-007.png differ
diff --git a/corpus/page-background/cold/read/page-007.xml b/corpus/page-background/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f6255c7de82987f898fd5b3afa20bdb00fac5dd9
--- /dev/null
+++ b/corpus/page-background/cold/read/page-007.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-background/cold/read/page-008.navigation.json b/corpus/page-background/cold/read/page-008.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..d2a29b6a0602e35842b0605919ee79aa85216a62
--- /dev/null
+++ b/corpus/page-background/cold/read/page-008.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "ColorRed - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/page-background/cold/read/page-008.png b/corpus/page-background/cold/read/page-008.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b309ee670f3838fcab2d007a7549ba9921aeec7
Binary files /dev/null and b/corpus/page-background/cold/read/page-008.png differ
diff --git a/corpus/page-background/cold/read/page-008.xml b/corpus/page-background/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2d6b4f741bc5f03ec29303610f43fbb8d56de2c1
--- /dev/null
+++ b/corpus/page-background/cold/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-background/cold/read/page-009.navigation.json b/corpus/page-background/cold/read/page-009.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..3cda85f0889301dcc34bd7f3046be58554002323
--- /dev/null
+++ b/corpus/page-background/cold/read/page-009.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "SmallGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/page-background/cold/read/page-009.png b/corpus/page-background/cold/read/page-009.png
new file mode 100644
index 0000000000000000000000000000000000000000..07af1f35fa610f3eb4fb07e679391ccc07846f74
Binary files /dev/null and b/corpus/page-background/cold/read/page-009.png differ
diff --git a/corpus/page-background/cold/read/page-009.xml b/corpus/page-background/cold/read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cdff2258459a79cf71abc3a86be1f028e65dbe19
--- /dev/null
+++ b/corpus/page-background/cold/read/page-009.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-background/cold/read/page-010.navigation.json b/corpus/page-background/cold/read/page-010.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..5c585338bca1397dd69f5878808de906cd5ff0ae
--- /dev/null
+++ b/corpus/page-background/cold/read/page-010.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Hidden - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/page-background/cold/read/page-010.png b/corpus/page-background/cold/read/page-010.png
new file mode 100644
index 0000000000000000000000000000000000000000..756322df669e4169f0910aadc989f3608b11c603
Binary files /dev/null and b/corpus/page-background/cold/read/page-010.png differ
diff --git a/corpus/page-background/cold/read/page-010.xml b/corpus/page-background/cold/read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b22513a98ff771e9450f944bcccc1dc1a187e55e
--- /dev/null
+++ b/corpus/page-background/cold/read/page-010.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/page-background/cold/read/payloads.json b/corpus/page-background/cold/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/page-background/cold/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/page-background/cold/run.json b/corpus/page-background/cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..2e035ab67362f60cf49d3ae690d4fca77cef7237
--- /dev/null
+++ b/corpus/page-background/cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/private/tmp/snowbound-page-background/candidate",
+ "expected_pages": 11,
+ "author": null,
+ "author_timeout_seconds": 600,
+ "inspect": false,
+ "collect_notebook": false,
+ "base": {
+ "file": "win7-office-base.qcow2",
+ "format": "qcow2",
+ "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
+ "virtual_size": 68719476736
+ },
+ "scripts": {
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/page-background/cold/scripts/cold.ps1 b/corpus/page-background/cold/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/page-background/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/page-background/cold/scripts/read.ps1 b/corpus/page-background/cold/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/page-background/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/page-background/cold/source.json b/corpus/page-background/cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..d05a2062f5d643ca05e92a4d82d237e2bc8e322a
--- /dev/null
+++ b/corpus/page-background/cold/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4808,
+ "sha256": "26e13bc41400333df9b3b6ebc182ea592bdc8a4bd0d3c33c0773e832965add13",
+ "mtime_ns": 1790669768851191042
+ },
+ {
+ "path": "Rules.one",
+ "bytes": 132704,
+ "sha256": "a513e56274ae794cf558da34dca7189225159d7eb6ab7d2256531fd7a2a55b44",
+ "mtime_ns": 1790671178905073494
+ }
+]
diff --git a/corpus/page-background/cold/teardown.json b/corpus/page-background/cold/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/page-background/cold/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/rule-lines/README.md b/corpus/rule-lines/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..7bb9931540f21c5337c2c9f2092040a582a71820
--- /dev/null
+++ b/corpus/rule-lines/README.md
@@ -0,0 +1,25 @@
+# Rule lines
+
+`native` is OneNote 2010's View, Rule Lines, set through its menu on the VM on eleven
+pages of a notebook the COM API made, each titled for what it was given: None; Narrow
+(ruled, then set back to None); College, Standard and Wide Ruled; Small, Medium, Large and
+Very Large Grid; ColorRed (Standard, then Rule Line Color, Red); Hidden (Standard, then
+Rule Line Color, ``, on a Teal page). MediumGrid was then given Rule Line Color,
+Purple. `read` is its cold read in a fresh clone.
+
+The page node stores six properties MS-ONE does not document, in this order: the
+horizontal lines' colour (`0x14001cd5`), kind (`0x14001cd3`: 2 ruled, 1 grid) and spacing
+(`0x14001cd4`, half inches), then the vertical lines' kind (`0x14001cd6`: 3 a margin line,
+1 grid), spacing (`0x14001cd7`, 0 for a margin line) and colour (`0x14001cd8`). Colours
+are 0xAARRGGBB with alpha 0xFF, unlike the page colour's COLORREF; the menu's ``
+colour is white. None removes all six. OneNote draws lines 1/96 inch wide: horizontal ones
+every spacing from the margin origin (36, 14.4) down across the page, grid lines through
+it either way down the whole page, and a margin line 1/96 inch left of it.
+`colors.txt` is each Rule Line Color as the page XML reports it, in the menu's order.
+
+`candidate` is `rule_lines_store_what_onenote_stores` in `crates/onestore/src/op/tests.rs`
+(`ONESTORE_RULE_LINES_EXPORT`): on OneNote's file, the `RuleLines` op gives None Wide
+Ruled, removes College's, turns Standard into a Small Grid and MediumGrid into Narrow
+Ruled in Red, and gives a created page, "Created", a Very Large Grid. `cold` is its fresh
+OneNote 2010 read: every page reads back with the lines written, and `read/page-*.png`
+shows them. `tools/test_rule_lines.py` checks both without a VM.
diff --git a/corpus/rule-lines/candidate/Open Notebook.onetoc2 b/corpus/rule-lines/candidate/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..2d2785048e719aaea688d47f540628bf86baaf99
Binary files /dev/null and b/corpus/rule-lines/candidate/Open Notebook.onetoc2 differ
diff --git a/corpus/rule-lines/candidate/Rules.one b/corpus/rule-lines/candidate/Rules.one
new file mode 100644
index 0000000000000000000000000000000000000000..7dc34ca30264afddb2c7ba621ac0db0de62bca82
Binary files /dev/null and b/corpus/rule-lines/candidate/Rules.one differ
diff --git a/corpus/rule-lines/cold/commands.jsonl b/corpus/rule-lines/cold/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..a57fb25a9ae5ee623cd7c84427a096998598f81e
--- /dev/null
+++ b/corpus/rule-lines/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-8F5199A7 -ExpectedPages 12 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 12 pages.\r\n", "stderr": "", "error": null}
+{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\read -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null}
diff --git a/corpus/rule-lines/cold/machine.json b/corpus/rule-lines/cold/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..ec2b821899d7763bac4e7011be60a2a03b8ec1de
--- /dev/null
+++ b/corpus/rule-lines/cold/machine.json
@@ -0,0 +1 @@
+{"name": "m6-8f5199a7", "hostname": "ONE-M6-8F5199A7"}
diff --git a/corpus/rule-lines/cold/read/environment.json b/corpus/rule-lines/cold/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..055ec1c4451571d914c2b975fee4511c3718f4f9
--- /dev/null
+++ b/corpus/rule-lines/cold/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-8F5199A7",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/rule-lines/cold/read/hierarchy.xml b/corpus/rule-lines/cold/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..30c09acf806fadc2496ade5d6f57238bcc31870b
--- /dev/null
+++ b/corpus/rule-lines/cold/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-000.navigation.json b/corpus/rule-lines/cold/read/page-000.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..3cda85f0889301dcc34bd7f3046be58554002323
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-000.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "SmallGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-000.png b/corpus/rule-lines/cold/read/page-000.png
new file mode 100644
index 0000000000000000000000000000000000000000..de61174ee173c0657bcdbfb25d916b91aaff71cf
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-000.png differ
diff --git a/corpus/rule-lines/cold/read/page-000.xml b/corpus/rule-lines/cold/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..124a8520ad7e73038f4554ea05cc0df3b45408e4
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-001.navigation.json b/corpus/rule-lines/cold/read/page-001.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..5c585338bca1397dd69f5878808de906cd5ff0ae
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-001.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Hidden - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-001.png b/corpus/rule-lines/cold/read/page-001.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d0adc8c83aab4d6bb05017c6de76f3f6c09b40f
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-001.png differ
diff --git a/corpus/rule-lines/cold/read/page-001.xml b/corpus/rule-lines/cold/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2f0641e053667cf0990e23808fa93f286280a218
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-001.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-002.navigation.json b/corpus/rule-lines/cold/read/page-002.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..6ea1c2e7cfb4aec222874c087cbbaf51b80cb3c7
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-002.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "LargeGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-002.png b/corpus/rule-lines/cold/read/page-002.png
new file mode 100644
index 0000000000000000000000000000000000000000..2f014d8b6bdcdc95b2f2d134d93e3a5b1df29d50
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-002.png differ
diff --git a/corpus/rule-lines/cold/read/page-002.xml b/corpus/rule-lines/cold/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..198a53ae38d8e74f392370cb79c361e95d01369c
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-002.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-003.navigation.json b/corpus/rule-lines/cold/read/page-003.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..74f74505c909f96ff84679b418cd721b7be7459f
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-003.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Standard - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-003.png b/corpus/rule-lines/cold/read/page-003.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f45c8f6ec22ab27524d71d7d1cd9d491d6bafb2
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-003.png differ
diff --git a/corpus/rule-lines/cold/read/page-003.xml b/corpus/rule-lines/cold/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..418545dd328f46d2f41f5c5b87cf50b0a3e71f3d
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-004.navigation.json b/corpus/rule-lines/cold/read/page-004.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..d2a29b6a0602e35842b0605919ee79aa85216a62
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-004.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "ColorRed - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-004.png b/corpus/rule-lines/cold/read/page-004.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d9891788240a977e303162e14dcf90e93aa1486
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-004.png differ
diff --git a/corpus/rule-lines/cold/read/page-004.xml b/corpus/rule-lines/cold/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ecba02cc6a08f4d1de4024aa208512e6710a51d1
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-004.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-005.navigation.json b/corpus/rule-lines/cold/read/page-005.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..5b9f18d07155f053a73f448d8ba8208522d981e4
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-005.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Created - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-005.png b/corpus/rule-lines/cold/read/page-005.png
new file mode 100644
index 0000000000000000000000000000000000000000..67bae3e09fdfa200beb1057810936a06df39686c
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-005.png differ
diff --git a/corpus/rule-lines/cold/read/page-005.xml b/corpus/rule-lines/cold/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bacea2ec47fbf7cfb4e0aded8093ab717354bee6
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-006.navigation.json b/corpus/rule-lines/cold/read/page-006.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..909744712d947f9080464d99bb701cd7de4071c8
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-006.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "VeryLargeGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-006.png b/corpus/rule-lines/cold/read/page-006.png
new file mode 100644
index 0000000000000000000000000000000000000000..515fc107961c0917c33e70ccf878b0006f80feab
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-006.png differ
diff --git a/corpus/rule-lines/cold/read/page-006.xml b/corpus/rule-lines/cold/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b80b7adb0c14e0439f4f33662a31c723a4c2f242
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-006.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-007.navigation.json b/corpus/rule-lines/cold/read/page-007.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..7ccafd7fd59c95368f51d575f411c3fb815d3863
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-007.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "MediumGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-007.png b/corpus/rule-lines/cold/read/page-007.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c261c2238293fc4d220516d5509ed4265ef278a
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-007.png differ
diff --git a/corpus/rule-lines/cold/read/page-007.xml b/corpus/rule-lines/cold/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..17aa5a9eb4cbd1d3086ac854e1bfd01aad0554da
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-007.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-008.navigation.json b/corpus/rule-lines/cold/read/page-008.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..007050b6d7a6917406f9ab04289d337fab5d8d43
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-008.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Wide - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-008.png b/corpus/rule-lines/cold/read/page-008.png
new file mode 100644
index 0000000000000000000000000000000000000000..2c0d0e6e7fe757c5e25514d342dbef2fbae4773e
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-008.png differ
diff --git a/corpus/rule-lines/cold/read/page-008.xml b/corpus/rule-lines/cold/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..3f623bae42d02d171ac17f4d03be05500d4beffd
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-009.navigation.json b/corpus/rule-lines/cold/read/page-009.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..7f25097d7917248dd57c3d10c5466fadaaa84a95
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-009.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Narrow - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-009.png b/corpus/rule-lines/cold/read/page-009.png
new file mode 100644
index 0000000000000000000000000000000000000000..33c5cf4c63329d3e6279ee4868044541bfd1dc43
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-009.png differ
diff --git a/corpus/rule-lines/cold/read/page-009.xml b/corpus/rule-lines/cold/read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..78108f6435b4c7ffd252bcd117126111fd5863a0
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-009.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-010.navigation.json b/corpus/rule-lines/cold/read/page-010.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..7824d62a06fb14853ccd8fca71946931cbe8e9ec
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-010.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "None - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-010.png b/corpus/rule-lines/cold/read/page-010.png
new file mode 100644
index 0000000000000000000000000000000000000000..13265708543cedff3b9df7dd32b3a1075a1a2f0a
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-010.png differ
diff --git a/corpus/rule-lines/cold/read/page-010.xml b/corpus/rule-lines/cold/read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..091b229025781813c5258cc08d2eb45c60b6a441
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-010.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/page-011.navigation.json b/corpus/rule-lines/cold/read/page-011.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..1d051ab18621c4450d424efbbd3056b504248b1b
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-011.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "College - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/read/page-011.png b/corpus/rule-lines/cold/read/page-011.png
new file mode 100644
index 0000000000000000000000000000000000000000..c55381d98a16b6012d27d1aef4b5e1087eb368e3
Binary files /dev/null and b/corpus/rule-lines/cold/read/page-011.png differ
diff --git a/corpus/rule-lines/cold/read/page-011.xml b/corpus/rule-lines/cold/read/page-011.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5851c2a885556a4940bc0f935017c462715e00e1
--- /dev/null
+++ b/corpus/rule-lines/cold/read/page-011.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/cold/read/payloads.json b/corpus/rule-lines/cold/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/rule-lines/cold/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/rule-lines/cold/run.json b/corpus/rule-lines/cold/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..d315390697e479b8177e1eedb2399f6dc5dad6e2
--- /dev/null
+++ b/corpus/rule-lines/cold/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/private/tmp/snowbound-rule-lines/candidate",
+ "expected_pages": 12,
+ "author": null,
+ "author_timeout_seconds": 600,
+ "inspect": false,
+ "collect_notebook": false,
+ "base": {
+ "file": "win7-office-base.qcow2",
+ "format": "qcow2",
+ "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346",
+ "virtual_size": 68719476736
+ },
+ "scripts": {
+ "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331",
+ "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41"
+ }
+}
diff --git a/corpus/rule-lines/cold/scripts/cold.ps1 b/corpus/rule-lines/cold/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/rule-lines/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/rule-lines/cold/scripts/read.ps1 b/corpus/rule-lines/cold/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/rule-lines/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/rule-lines/cold/source.json b/corpus/rule-lines/cold/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..13632db263c2a5049d67291facf40b885f23f14b
--- /dev/null
+++ b/corpus/rule-lines/cold/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4808,
+ "sha256": "26e13bc41400333df9b3b6ebc182ea592bdc8a4bd0d3c33c0773e832965add13",
+ "mtime_ns": 1790669621631553306
+ },
+ {
+ "path": "Rules.one",
+ "bytes": 93264,
+ "sha256": "3ab2a2abcef76f4e008e349560caa63a907e15a61e71d080dd3e764c4aca34c1",
+ "mtime_ns": 1790669717172032242
+ }
+]
diff --git a/corpus/rule-lines/cold/teardown.json b/corpus/rule-lines/cold/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/rule-lines/cold/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/corpus/rule-lines/native/colors.txt b/corpus/rule-lines/native/colors.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5e40850773c4b91ce2f69a01f543994cdf6a2d0b
--- /dev/null
+++ b/corpus/rule-lines/native/colors.txt
@@ -0,0 +1,20 @@
+none #FFFFFF
+Light Blue #CAEBFD
+Blue #D1E5FC
+Yellow #FFF2AE
+Green #D9E2C7
+Red #FFD4D6
+Purple #E0C9FF
+Cyan #BBE8DB
+Orange #F5DFB4
+Magenta #F5D3E8
+Blue Mist #BED3E2
+Purple Mist #C5BACD
+Tan #F1E3C2
+Lemon #F9FBA9
+Apple #CFF1BF
+Teal #94D7DF
+Red Chalk #D6ACAC
+Silver #C8C8D3
+
+[exited with code 0]
diff --git a/corpus/rule-lines/native/commands.jsonl b/corpus/rule-lines/native/commands.jsonl
new file mode 100644
index 0000000000000000000000000000000000000000..f6306365d143bbae3d5a07444aec5eb8380a6caf
--- /dev/null
+++ b/corpus/rule-lines/native/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-4E581665 -ExpectedPages 11 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 11 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/rule-lines/native/machine.json b/corpus/rule-lines/native/machine.json
new file mode 100644
index 0000000000000000000000000000000000000000..0e6ceda6fd4916a255a2a8fe184daa15efde0d04
--- /dev/null
+++ b/corpus/rule-lines/native/machine.json
@@ -0,0 +1 @@
+{"name": "m6-4e581665", "hostname": "ONE-M6-4E581665"}
diff --git a/corpus/rule-lines/native/notebook/Open Notebook.onetoc2 b/corpus/rule-lines/native/notebook/Open Notebook.onetoc2
new file mode 100644
index 0000000000000000000000000000000000000000..2d2785048e719aaea688d47f540628bf86baaf99
Binary files /dev/null and b/corpus/rule-lines/native/notebook/Open Notebook.onetoc2 differ
diff --git a/corpus/rule-lines/native/notebook/Rules.one b/corpus/rule-lines/native/notebook/Rules.one
new file mode 100644
index 0000000000000000000000000000000000000000..34b827282dfe81dbbee916a11083169bc6c32eec
Binary files /dev/null and b/corpus/rule-lines/native/notebook/Rules.one differ
diff --git a/corpus/rule-lines/native/read/environment.json b/corpus/rule-lines/native/read/environment.json
new file mode 100644
index 0000000000000000000000000000000000000000..fa14e9d48787948cdd11bcd302d49048bbde3852
--- /dev/null
+++ b/corpus/rule-lines/native/read/environment.json
@@ -0,0 +1,7 @@
+{
+ "powershell": "5.1.14409.1005",
+ "schema": "xs2010",
+ "hostname": "ONE-M6-4E581665",
+ "cold": true,
+ "onenote": "14.0.4763.1000"
+}
diff --git a/corpus/rule-lines/native/read/hierarchy.xml b/corpus/rule-lines/native/read/hierarchy.xml
new file mode 100644
index 0000000000000000000000000000000000000000..13e0022d58f6d5bd520e0d1f6a59d07a11bb6cbc
--- /dev/null
+++ b/corpus/rule-lines/native/read/hierarchy.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/page-000.navigation.json b/corpus/rule-lines/native/read/page-000.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..74f74505c909f96ff84679b418cd721b7be7459f
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-000.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Standard - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/native/read/page-000.png b/corpus/rule-lines/native/read/page-000.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3a1d429e9398f0bb0543f820fbf314a02a32104
Binary files /dev/null and b/corpus/rule-lines/native/read/page-000.png differ
diff --git a/corpus/rule-lines/native/read/page-000.xml b/corpus/rule-lines/native/read/page-000.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a0df7113ab49f1a57435b0fc8b08e8b63de2dcfb
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-000.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/page-001.navigation.json b/corpus/rule-lines/native/read/page-001.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..6ea1c2e7cfb4aec222874c087cbbaf51b80cb3c7
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-001.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "LargeGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/native/read/page-001.png b/corpus/rule-lines/native/read/page-001.png
new file mode 100644
index 0000000000000000000000000000000000000000..883f096ccb28b9bdb9d4739ce19b95e9839faf84
Binary files /dev/null and b/corpus/rule-lines/native/read/page-001.png differ
diff --git a/corpus/rule-lines/native/read/page-001.xml b/corpus/rule-lines/native/read/page-001.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8244fb2b57cdd120640d46f3e2d36b092cd27819
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-001.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/page-002.navigation.json b/corpus/rule-lines/native/read/page-002.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..d2a29b6a0602e35842b0605919ee79aa85216a62
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-002.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "ColorRed - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/native/read/page-002.png b/corpus/rule-lines/native/read/page-002.png
new file mode 100644
index 0000000000000000000000000000000000000000..4fd3402c8f4412c13661cc76347ce0905e9138f3
Binary files /dev/null and b/corpus/rule-lines/native/read/page-002.png differ
diff --git a/corpus/rule-lines/native/read/page-002.xml b/corpus/rule-lines/native/read/page-002.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f6560ac65325e720b8bfa8db9c880a088520dc85
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-002.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/page-003.navigation.json b/corpus/rule-lines/native/read/page-003.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..3cda85f0889301dcc34bd7f3046be58554002323
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-003.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "SmallGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/native/read/page-003.png b/corpus/rule-lines/native/read/page-003.png
new file mode 100644
index 0000000000000000000000000000000000000000..4053718977f0cffa04829949d7b296d91094e80c
Binary files /dev/null and b/corpus/rule-lines/native/read/page-003.png differ
diff --git a/corpus/rule-lines/native/read/page-003.xml b/corpus/rule-lines/native/read/page-003.xml
new file mode 100644
index 0000000000000000000000000000000000000000..65ad5a4d6d7e7b19d14273f7b1cce442e8af56a4
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-003.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/page-004.navigation.json b/corpus/rule-lines/native/read/page-004.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..5c585338bca1397dd69f5878808de906cd5ff0ae
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-004.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Hidden - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/native/read/page-004.png b/corpus/rule-lines/native/read/page-004.png
new file mode 100644
index 0000000000000000000000000000000000000000..c84a8ecdc67d7095fa031637f0b94d4f8157fd41
Binary files /dev/null and b/corpus/rule-lines/native/read/page-004.png differ
diff --git a/corpus/rule-lines/native/read/page-004.xml b/corpus/rule-lines/native/read/page-004.xml
new file mode 100644
index 0000000000000000000000000000000000000000..406270390b698e7a377e388277c99ec489afcde0
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-004.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/page-005.navigation.json b/corpus/rule-lines/native/read/page-005.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..1d051ab18621c4450d424efbbd3056b504248b1b
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-005.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "College - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/native/read/page-005.png b/corpus/rule-lines/native/read/page-005.png
new file mode 100644
index 0000000000000000000000000000000000000000..15a17cd81784b75f9473751cb317482311282411
Binary files /dev/null and b/corpus/rule-lines/native/read/page-005.png differ
diff --git a/corpus/rule-lines/native/read/page-005.xml b/corpus/rule-lines/native/read/page-005.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f6ee073a558c8cbf2b5a52afa8b92953c5409d6b
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-005.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/page-006.navigation.json b/corpus/rule-lines/native/read/page-006.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..7824d62a06fb14853ccd8fca71946931cbe8e9ec
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-006.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "None - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/native/read/page-006.png b/corpus/rule-lines/native/read/page-006.png
new file mode 100644
index 0000000000000000000000000000000000000000..49f2ff077f49645759f31067633c716ae08fb6b3
Binary files /dev/null and b/corpus/rule-lines/native/read/page-006.png differ
diff --git a/corpus/rule-lines/native/read/page-006.xml b/corpus/rule-lines/native/read/page-006.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b68025658b6b35362b22b0b1ab2c4fb99410f067
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-006.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/page-007.navigation.json b/corpus/rule-lines/native/read/page-007.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..909744712d947f9080464d99bb701cd7de4071c8
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-007.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "VeryLargeGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/native/read/page-007.png b/corpus/rule-lines/native/read/page-007.png
new file mode 100644
index 0000000000000000000000000000000000000000..680352c4729c2852b98aebc3f80d2ca780109e78
Binary files /dev/null and b/corpus/rule-lines/native/read/page-007.png differ
diff --git a/corpus/rule-lines/native/read/page-007.xml b/corpus/rule-lines/native/read/page-007.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ef82b34ec9eb3e7fc176d78f4fae978fd42bd00e
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-007.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/page-008.navigation.json b/corpus/rule-lines/native/read/page-008.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..007050b6d7a6917406f9ab04289d337fab5d8d43
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-008.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Wide - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/native/read/page-008.png b/corpus/rule-lines/native/read/page-008.png
new file mode 100644
index 0000000000000000000000000000000000000000..b5904a87db31d072e8310400fc91a8dcef1d4e72
Binary files /dev/null and b/corpus/rule-lines/native/read/page-008.png differ
diff --git a/corpus/rule-lines/native/read/page-008.xml b/corpus/rule-lines/native/read/page-008.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a2caacb0e3949c4305deb42f97dfb66dafc4f5ed
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-008.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/page-009.navigation.json b/corpus/rule-lines/native/read/page-009.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..7ccafd7fd59c95368f51d575f411c3fb815d3863
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-009.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "MediumGrid - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/native/read/page-009.png b/corpus/rule-lines/native/read/page-009.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec1bda13876e081cd3b5a3c8eb5b755a75ba35a7
Binary files /dev/null and b/corpus/rule-lines/native/read/page-009.png differ
diff --git a/corpus/rule-lines/native/read/page-009.xml b/corpus/rule-lines/native/read/page-009.xml
new file mode 100644
index 0000000000000000000000000000000000000000..5901aa0663735b816f64d1659fd95e5b751eb58b
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-009.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/page-010.navigation.json b/corpus/rule-lines/native/read/page-010.navigation.json
new file mode 100644
index 0000000000000000000000000000000000000000..7f25097d7917248dd57c3d10c5466fadaaa84a95
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-010.navigation.json
@@ -0,0 +1,13 @@
+{
+ "exit": 0,
+ "stdout": "",
+ "stderr": "",
+ "w": 800,
+ "h": 600,
+ "error": null,
+ "win": {
+ "title": "Narrow - Microsoft OneNote",
+ "class": "Framework::CFrame",
+ "dialog": false
+ }
+}
\ No newline at end of file
diff --git a/corpus/rule-lines/native/read/page-010.png b/corpus/rule-lines/native/read/page-010.png
new file mode 100644
index 0000000000000000000000000000000000000000..d3f54fd498c8db04015a5c997dc146f1d32f4094
Binary files /dev/null and b/corpus/rule-lines/native/read/page-010.png differ
diff --git a/corpus/rule-lines/native/read/page-010.xml b/corpus/rule-lines/native/read/page-010.xml
new file mode 100644
index 0000000000000000000000000000000000000000..06126160d1b0c06656c34e906fa57d3731d51d6e
--- /dev/null
+++ b/corpus/rule-lines/native/read/page-010.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/corpus/rule-lines/native/read/payloads.json b/corpus/rule-lines/native/read/payloads.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843
--- /dev/null
+++ b/corpus/rule-lines/native/read/payloads.json
@@ -0,0 +1,3 @@
+[
+
+]
\ No newline at end of file
diff --git a/corpus/rule-lines/native/run.json b/corpus/rule-lines/native/run.json
new file mode 100644
index 0000000000000000000000000000000000000000..c65f7ec6157d71a321b709bf2895cb796d08d02d
--- /dev/null
+++ b/corpus/rule-lines/native/run.json
@@ -0,0 +1,18 @@
+{
+ "notebook": "/private/tmp/snowbound-rule-lines/lab/native-notebook",
+ "expected_pages": 11,
+ "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/rule-lines/native/scripts/cold.ps1 b/corpus/rule-lines/native/scripts/cold.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f
--- /dev/null
+++ b/corpus/rule-lines/native/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/rule-lines/native/scripts/read.ps1 b/corpus/rule-lines/native/scripts/read.ps1
new file mode 100644
index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b
--- /dev/null
+++ b/corpus/rule-lines/native/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/rule-lines/native/source.json b/corpus/rule-lines/native/source.json
new file mode 100644
index 0000000000000000000000000000000000000000..55e0e1c2a1eb579021f1209cbef55aded7ba18bb
--- /dev/null
+++ b/corpus/rule-lines/native/source.json
@@ -0,0 +1,14 @@
+[
+ {
+ "path": "Open Notebook.onetoc2",
+ "bytes": 4808,
+ "sha256": "26e13bc41400333df9b3b6ebc182ea592bdc8a4bd0d3c33c0773e832965add13",
+ "mtime_ns": 1790669502093308352
+ },
+ {
+ "path": "Rules.one",
+ "bytes": 88432,
+ "sha256": "76da013db8a7015b9cb864dd354cb0bb184c9ca3b5e76b755b1143c830ca39b1",
+ "mtime_ns": 1790669501695822834
+ }
+]
diff --git a/corpus/rule-lines/native/teardown.json b/corpus/rule-lines/native/teardown.json
new file mode 100644
index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70
--- /dev/null
+++ b/corpus/rule-lines/native/teardown.json
@@ -0,0 +1 @@
+{"absent": true}
diff --git a/crates/canvas/assets/tags/blog.svg b/crates/canvas/assets/tags/blog.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b982d25e89fb2b8e29aed8f69ba13e6014cc222b
--- /dev/null
+++ b/crates/canvas/assets/tags/blog.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/book.svg b/crates/canvas/assets/tags/book.svg
new file mode 100644
index 0000000000000000000000000000000000000000..15f5e3ed5ea86e0573e15c48db878446198f43e8
--- /dev/null
+++ b/crates/canvas/assets/tags/book.svg
@@ -0,0 +1,12 @@
+
diff --git a/crates/canvas/assets/tags/box-small.svg b/crates/canvas/assets/tags/box-small.svg
new file mode 100644
index 0000000000000000000000000000000000000000..17ed0f99c5a134e131cf29b3392fb1940423c000
--- /dev/null
+++ b/crates/canvas/assets/tags/box-small.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/check-small.svg b/crates/canvas/assets/tags/check-small.svg
new file mode 100644
index 0000000000000000000000000000000000000000..85254d3e1bd50de3007c6fbc96174fd048ee8943
--- /dev/null
+++ b/crates/canvas/assets/tags/check-small.svg
@@ -0,0 +1,6 @@
+
diff --git a/crates/canvas/assets/tags/email.svg b/crates/canvas/assets/tags/email.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2e3378b2e99d99468a2c7d7805ca5ccbd94cfdd3
--- /dev/null
+++ b/crates/canvas/assets/tags/email.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/idea.svg b/crates/canvas/assets/tags/idea.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f210d6a6d1949443a7448ed32697ef03393d917d
--- /dev/null
+++ b/crates/canvas/assets/tags/idea.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/mark-arrow.svg b/crates/canvas/assets/tags/mark-arrow.svg
new file mode 100644
index 0000000000000000000000000000000000000000..97c2c3c44577ef5dad304ca3244098182964baa4
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-arrow.svg
@@ -0,0 +1,6 @@
+
diff --git a/crates/canvas/assets/tags/mark-client.svg b/crates/canvas/assets/tags/mark-client.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b562e9082ec9bf2d0e6f6dad91a46658025f3f36
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-client.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/mark-manager.svg b/crates/canvas/assets/tags/mark-manager.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c7b1b3e74d3f5454151a4bd08edcabb2f54a4dee
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-manager.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/mark-one.svg b/crates/canvas/assets/tags/mark-one.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f95d8aa3643bd45b9b71e6c9d4756543526d9c00
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-one.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/mark-person.svg b/crates/canvas/assets/tags/mark-person.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1f2d6f0affa1495cf724a9825b126c6d175e5a81
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-person.svg
@@ -0,0 +1,9 @@
+
diff --git a/crates/canvas/assets/tags/mark-two.svg b/crates/canvas/assets/tags/mark-two.svg
new file mode 100644
index 0000000000000000000000000000000000000000..43c86f79bd98b9127b769d353f8089e1f89d1294
--- /dev/null
+++ b/crates/canvas/assets/tags/mark-two.svg
@@ -0,0 +1,7 @@
+
diff --git a/crates/canvas/assets/tags/movie.svg b/crates/canvas/assets/tags/movie.svg
new file mode 100644
index 0000000000000000000000000000000000000000..1dcbbb38994002babdf78dc8ca0c5dd3866d340e
--- /dev/null
+++ b/crates/canvas/assets/tags/movie.svg
@@ -0,0 +1,10 @@
+
diff --git a/crates/canvas/assets/tags/password.svg b/crates/canvas/assets/tags/password.svg
new file mode 100644
index 0000000000000000000000000000000000000000..b32cbb4abd2d8df60314e36c08cec7e6f3fedbe8
--- /dev/null
+++ b/crates/canvas/assets/tags/password.svg
@@ -0,0 +1,11 @@
+
diff --git a/crates/canvas/assets/tags/web.svg b/crates/canvas/assets/tags/web.svg
new file mode 100644
index 0000000000000000000000000000000000000000..716ae57eeebe22fe7d39e0790c5ff80b24c23007
--- /dev/null
+++ b/crates/canvas/assets/tags/web.svg
@@ -0,0 +1,14 @@
+
diff --git a/crates/canvas/src/editor.rs b/crates/canvas/src/editor.rs
index 3a65e3bfdc75bb4e44bf269b6ee07125e2118e42..eb296741af44f2ac8598b4089186579b11ed943d 100644
--- a/crates/canvas/src/editor.rs
+++ b/crates/canvas/src/editor.rs
@@ -31,6 +31,7 @@ mod format;
mod link;
mod ops;
pub(crate) mod page;
+mod space;
mod table;
pub use format::{
Alignment, BULLET_LIBRARY, FormatState, Formatting, ListStyle, NUMBER_LIBRARY, NoteTag, Toggle,
@@ -126,6 +127,7 @@ struct PageHeader {
created: Option,
margin_origin: [f32; 2],
color: Option,
+ rule_lines: Option,
areas: Vec,
}
@@ -754,6 +756,11 @@ enum RestoreFocus {
enum History {
Date(Box),
+ /// The page's colour and rule lines before a change to either.
+ Paper {
+ color: Option,
+ rule_lines: Option,
+ },
Draft {
outlines: Box<[TextOutline; 2]>,
index: usize,
@@ -765,10 +772,25 @@ enum History {
outline: onestore::ExGuid,
change: Box,
},
+ /// A page object's position: an outline, picture or ink drawing.
Position {
- outline: onestore::ExGuid,
+ object: onestore::ExGuid,
position: [Option; 2],
},
+ /// Parts `outline`'s paragraphs from root node `at` into outline `part` at `position`.
+ Split {
+ outline: ExGuid,
+ at: usize,
+ part: ExGuid,
+ position: [f32; 2],
+ },
+ /// Returns `part`'s paragraphs to the end of `outline` and removes it: each root of
+ /// `part` under the parent it had, the outline when `None`.
+ Join {
+ outline: ExGuid,
+ part: ExGuid,
+ parents: Vec<(ExGuid, Option)>,
+ },
Layout {
outline: ExGuid,
layout: onestore::document::Layout,
@@ -797,8 +819,12 @@ enum History {
selection: Selection,
focus: RestoreFocus,
},
- /// One undo step of edits to OneNote's page selection, undone last first.
- Group(Vec),
+ /// One undo step of several edits, undone last first; `page` when they edited OneNote's
+ /// page selection, which undoing selects again.
+ Group {
+ entries: Vec,
+ page: bool,
+ },
}
impl CanvasEditor {
@@ -876,6 +902,7 @@ impl CanvasEditor {
created: page.created,
margin_origin: page.margin_origin,
color: page.color,
+ rule_lines: page.rule_lines,
areas,
};
let mut ids = BTreeSet::new();
@@ -1019,6 +1046,7 @@ impl CanvasEditor {
let gone = |focus: &RestoreFocus| matches!(focus, RestoreFocus::Outline(id) if !fresh.outlines.iter().any(|o| o.id == *id));
let reaches_one = |history: &History| match history {
History::Date(_) => date_changed,
+ History::Paper { .. } => false,
History::Image { .. } | History::Picture { .. } => objects_changed,
History::Draft { outlines, .. } => changed.contains(&outlines[0].id),
History::Text { outline, change } => {
@@ -1028,16 +1056,18 @@ impl CanvasEditor {
.iter()
.any(|placement| objects_changed || changed.contains(&placement.id))
}
- History::Position { outline, .. } | History::Layout { outline, .. } => {
- changed.contains(outline)
+ History::Position { object, .. } => changed.contains(object) || objects_changed,
+ History::Layout { outline, .. } => changed.contains(outline),
+ History::Split { outline, part, .. } | History::Join { outline, part, .. } => {
+ changed.contains(outline) || changed.contains(part) || objects_changed
}
History::Remove { outline, focus } => changed.contains(outline) || gone(focus),
History::Insert { outline, focus, .. } => changed.contains(&outline.id) || gone(focus),
History::Restore { source, focus, .. } => changed.contains(&source.id) || gone(focus),
- History::Group(_) => unreachable!("groups do not nest"),
+ History::Group { .. } => unreachable!("groups do not nest"),
};
let reaches = |history: &History| match history {
- History::Group(entries) => entries.iter().any(reaches_one),
+ History::Group { entries, .. } => entries.iter().any(reaches_one),
history => reaches_one(history),
};
fresh.undo = std::mem::take(&mut self.undo)
@@ -1143,6 +1173,7 @@ impl CanvasEditor {
.or(self.header.created),
margin_origin: self.header.margin_origin,
color: self.header.color,
+ rule_lines: self.header.rule_lines,
objects,
definitions,
})
@@ -1215,6 +1246,11 @@ impl CanvasEditor {
self.header.color
}
+ /// The page's rule lines (View, Rule Lines).
+ pub fn rule_lines(&self) -> Option {
+ self.header.rule_lines
+ }
+
/// Where the page's first body line starts, in points, under its title.
pub fn body_start(&self) -> Option<[f32; 2]> {
let title = self
@@ -1419,6 +1455,44 @@ impl CanvasEditor {
Ok(true)
}
+ /// Gives the page `color` (View, Page Color) and `rule_lines` (View, Rule Lines) as one
+ /// undo step; false when it had them.
+ pub fn set_paper(
+ &mut self,
+ color: Option,
+ rule_lines: Option,
+ ) -> bool {
+ if (color, rule_lines) == (self.header.color, self.header.rule_lines) {
+ return false;
+ }
+ self.finish_composition();
+ let before = self.paper(color, rule_lines);
+ self.undo.push(before);
+ self.redo.clear();
+ true
+ }
+
+ /// Shows `color` and `rule_lines`, recording the ops that store them; the history entry
+ /// restoring what the page had.
+ fn paper(
+ &mut self,
+ color: Option,
+ rule_lines: Option,
+ ) -> History {
+ let mut ops = Vec::new();
+ if color != self.header.color {
+ ops.push(PageOp::Color(color));
+ }
+ if rule_lines != self.header.rule_lines {
+ ops.push(PageOp::RuleLines(rule_lines));
+ }
+ self.record(Ok(ops));
+ History::Paper {
+ color: std::mem::replace(&mut self.header.color, color),
+ rule_lines: std::mem::replace(&mut self.header.rule_lines, rule_lines),
+ }
+ }
+
/// Stored content; arrow-created paragraphs appear only in `visible_outlines`.
pub fn outlines(&self) -> &[TextOutline] {
&self.outlines
@@ -1665,7 +1739,7 @@ impl CanvasEditor {
let layout = &mut self.outlines[index].layout;
let previous = [layout.x, layout.y];
self.undo.push(History::Position {
- outline: id,
+ object: id,
position: previous,
});
self.redo.clear();
@@ -2145,7 +2219,10 @@ impl CanvasEditor {
let result = edit(self);
let entries = self.undo.split_off(depth);
if !entries.is_empty() {
- self.undo.push(History::Group(entries));
+ self.undo.push(History::Group {
+ entries,
+ page: true,
+ });
}
result
}
@@ -3212,10 +3289,9 @@ impl CanvasEditor {
};
match self.apply_history(engine, history) {
Ok(inverse) => {
- let group = matches!(inverse, History::Group(_));
+ let page = matches!(inverse, History::Group { page: true, .. });
self.redo.push(inverse);
- // Undoing an edit to the page selection selects the page again.
- if group {
+ if page {
self.select_page()?;
}
Ok(true)
@@ -3255,6 +3331,7 @@ impl CanvasEditor {
.date
.as_ref()
.is_some_and(|current| current.source().id == date.source().id),
+ History::Paper { .. } => true,
History::Draft {
outlines, index, ..
} => {
@@ -3265,11 +3342,22 @@ impl CanvasEditor {
.position(|item| item.id == outlines[0].id)
.is_none_or(|found| found == *index)
}
- History::Text { outline, .. }
- | History::Position { outline, .. }
- | History::Layout { outline, .. } => {
+ History::Text { outline, .. } | History::Layout { outline, .. } => {
self.outlines.iter().any(|item| item.id == *outline)
}
+ History::Position { object, .. } => self.object_layout_mut(*object).is_some(),
+ History::Split {
+ outline, at, part, ..
+ } => {
+ self.outlines
+ .iter()
+ .find(|item| item.id == *outline)
+ .is_some_and(|item| (1..item.document.nodes().len()).contains(at))
+ && !self.outlines.iter().any(|item| item.id == *part)
+ }
+ History::Join { outline, part, .. } => [outline, part]
+ .iter()
+ .all(|id| self.outlines.iter().any(|item| item.id == **id)),
History::Image { image, .. } => self.image(*image).is_some(),
History::Picture {
index,
@@ -3290,7 +3378,7 @@ impl CanvasEditor {
History::Insert { index, .. } | History::Restore { index, .. } => {
*index <= self.outlines.len()
}
- History::Group(_) => true,
+ History::Group { .. } => true,
};
if !valid {
return Err((history, EditError::InvalidRange.into()));
@@ -3301,6 +3389,7 @@ impl CanvasEditor {
self.record(Ok(self.date_ops()));
History::Date(Box::new(shown))
}
+ History::Paper { color, rule_lines } => self.paper(color, rule_lines),
History::Draft {
outlines,
index,
@@ -3387,25 +3476,67 @@ impl CanvasEditor {
change: Box::new(inverse),
}
}
- History::Position { outline, position } => {
- let index = self
- .outlines
- .iter()
- .position(|item| item.id == outline)
- .unwrap();
- let layout = &mut self.outlines[index].layout;
+ History::Position { object, position } => {
+ let layout = self.object_layout_mut(object).unwrap();
let previous = [layout.x, layout.y];
[layout.x, layout.y] = position;
- self.active = Focus::Outline(index);
+ if let Some(index) = self.outlines.iter().position(|item| item.id == object) {
+ self.active = Focus::Outline(index);
+ }
self.record(self.placement_ops(&Placement {
- id: outline,
+ id: object,
position: previous,
}));
History::Position {
- outline,
+ object,
position: previous,
}
}
+ History::Split {
+ outline,
+ at,
+ part,
+ position,
+ } => match self.split_outline(engine, outline, at, part, position) {
+ Ok(parents) => History::Join {
+ outline,
+ part,
+ parents,
+ },
+ Err(error) => {
+ return Err((
+ History::Split {
+ outline,
+ at,
+ part,
+ position,
+ },
+ error,
+ ));
+ }
+ },
+ History::Join {
+ outline,
+ part,
+ parents,
+ } => match self.join_outline(engine, outline, part, &parents) {
+ Ok((at, position)) => History::Split {
+ outline,
+ at,
+ part,
+ position,
+ },
+ Err(error) => {
+ return Err((
+ History::Join {
+ outline,
+ part,
+ parents,
+ },
+ error,
+ ));
+ }
+ },
History::Layout { outline, layout } => {
let index = self
.outlines
@@ -3554,18 +3685,21 @@ impl CanvasEditor {
self.active = Focus::Outline(index);
History::Remove { outline: id, focus }
}
- History::Group(mut entries) => {
+ History::Group { mut entries, page } => {
let mut inverses = Vec::with_capacity(entries.len());
while let Some(entry) = entries.pop() {
match self.apply_history(engine, entry) {
Ok(inverse) => inverses.push(inverse),
Err((entry, error)) => {
entries.push(entry);
- return Err((History::Group(entries), error));
+ return Err((History::Group { entries, page }, error));
}
}
}
- History::Group(inverses)
+ History::Group {
+ entries: inverses,
+ page,
+ }
}
};
self.preferred_x = None;
@@ -5445,6 +5579,7 @@ mod tests {
created: None,
margin_origin: [36.0, 14.4],
color: None,
+ rule_lines: None,
definitions: BTreeMap::new(),
objects: vec![PageObject::Image(background), PageObject::Image(picture)],
},
@@ -5569,6 +5704,7 @@ mod tests {
created: None,
margin_origin: [36.0, 14.4],
color: None,
+ rule_lines: None,
definitions: BTreeMap::new(),
objects,
},
@@ -5627,6 +5763,7 @@ mod tests {
created: None,
margin_origin: [36.0, 14.4],
color: None,
+ rule_lines: None,
definitions: BTreeMap::new(),
objects: vec![PageObject::Outline(source)],
},
@@ -5767,6 +5904,7 @@ mod tests {
created: None,
margin_origin: [36.0, 14.4],
color: None,
+ rule_lines: None,
definitions: BTreeMap::new(),
objects: vec![
PageObject::Outline(mixed),
@@ -5858,6 +5996,7 @@ mod tests {
created: None,
margin_origin: [36.0, 14.4],
color: None,
+ rule_lines: None,
definitions: BTreeMap::new(),
objects,
},
diff --git a/crates/canvas/src/editor/format.rs b/crates/canvas/src/editor/format.rs
index 017ffa75775bc9c860c5219426eae7d65e69ad7b..222e2181b7edf55fea5428592cf22c015e157614 100644
--- a/crates/canvas/src/editor/format.rs
+++ b/crates/canvas/src/editor/format.rs
@@ -1895,13 +1895,19 @@ mod tests {
|editor: &CanvasEditor| editor.active_outline().shaped.paragraphs[1].tags[0].icon;
assert_eq!(
icon(&editor),
- crate::outline::TagIcon::CheckBox { checked: false }
+ crate::outline::TagIcon::CheckBox {
+ checked: false,
+ mark: None
+ }
);
editor.format(&mut engine, Formatting::Check).unwrap();
assert_eq!(text_tags(&editor, 1), [("To Do".into(), 1, true)]);
assert_eq!(
icon(&editor),
- crate::outline::TagIcon::CheckBox { checked: true }
+ crate::outline::TagIcon::CheckBox {
+ checked: true,
+ mark: None
+ }
);
editor.select([at(1, 0); 2].into()).unwrap();
editor.format(&mut engine, Formatting::Check).unwrap();
@@ -1976,7 +1982,10 @@ mod tests {
assert_eq!(
icons[..7],
[
- TagIcon::CheckBox { checked: false },
+ TagIcon::CheckBox {
+ checked: false,
+ mark: None
+ },
TagIcon::Star,
TagIcon::Question,
TagIcon::Highlight,
diff --git a/crates/canvas/src/editor/ops.rs b/crates/canvas/src/editor/ops.rs
index bc90efe1505d5de4288c21320a2d805d557d8429..3b12789442eaaf253b8183ca31427b8fcb319613 100644
--- a/crates/canvas/src/editor/ops.rs
+++ b/crates/canvas/src/editor/ops.rs
@@ -173,7 +173,7 @@ fn lower(
/// An empty paragraph keeping a container from emptying while its paragraphs are removed
/// and inserted again; the lowering removes it after.
-fn anchor() -> Result {
+pub(super) fn anchor() -> Result {
crate::document::node(
Paragraph::new(String::new(), Format::default()),
Format::default(),
@@ -327,6 +327,9 @@ impl CanvasEditor {
for object in &self.objects {
match object {
page::Content::Image(image) if image.id == placement.id => return picture(image),
+ page::Content::Ink(ink) if ink.id == placement.id => {
+ return layout_ops(ink.id, &old, &moved(&ink.layout));
+ }
page::Content::Outline { source, .. } if source.id == placement.id => {
return layout_ops(source.id, &old, &moved(&source.layout));
}
@@ -352,29 +355,7 @@ impl CanvasEditor {
match (old, new) {
(None, None) => Ok(Vec::new()),
(Some(_), None) => Ok(vec![PageOp::Delete { object: id }]),
- (None, Some(new)) => {
- let outline = new.snapshot();
- let page = |objects| Page {
- title: String::new(),
- identity: None,
- created: None,
- margin_origin: [0.0; 2],
- color: None,
- objects,
- definitions: named(&self.definitions, &[&outline.paragraphs]),
- };
- let mut ops = op::lower_page(
- &page(Vec::new()),
- &page(vec![PageObject::Outline(outline.clone())]),
- )?;
- let successor = self.successor(id);
- for op in &mut ops {
- if let PageOp::Add { before, .. } = op {
- *before = successor;
- }
- }
- Ok(ops)
- }
+ (None, Some(new)) => self.added(new.snapshot()),
(Some(old), Some(new)) => {
let (a, b) = (old.document.nodes(), new.document.nodes());
let prefix = a.iter().zip(b).take_while(|(x, y)| x == y).count();
@@ -397,6 +378,31 @@ impl CanvasEditor {
}
}
+ /// Ops adding `outline`, new to the stored page, in its place in paint order.
+ pub(super) fn added(&self, outline: Outline) -> Lowered {
+ let page = |objects| Page {
+ title: String::new(),
+ identity: None,
+ created: None,
+ margin_origin: [0.0; 2],
+ color: None,
+ rule_lines: None,
+ objects,
+ definitions: named(&self.definitions, &[&outline.paragraphs]),
+ };
+ let mut ops = op::lower_page(
+ &page(Vec::new()),
+ &page(vec![PageObject::Outline(outline.clone())]),
+ )?;
+ let successor = self.successor(outline.id);
+ for op in &mut ops {
+ if let PageOp::Add { before, .. } = op {
+ *before = successor;
+ }
+ }
+ Ok(ops)
+ }
+
/// The page's date as the editor shows it.
pub(super) fn date_ops(&self) -> Vec {
let Some(date) = &self.date else {
diff --git a/crates/canvas/src/editor/page.rs b/crates/canvas/src/editor/page.rs
index bc45ba4267550f186b35a88b473eced13df66a53..a90a91a003cc6ed0f9ec986a51272bdf818f231d 100644
--- a/crates/canvas/src/editor/page.rs
+++ b/crates/canvas/src/editor/page.rs
@@ -76,9 +76,9 @@ impl Content {
match self {
Self::Outline { source, .. } => Some((source.id, &mut source.layout)),
Self::Image(source) => Some((source.id, &mut source.layout)),
+ Self::Ink(source) => Some((source.id, &mut source.layout)),
Self::ReadOnly(object) => Some((object.source.id(), object.source.layout_mut())),
- // Strokes carry their own page coordinates, so a layout move would not move them.
- Self::Date { .. } | Self::Ink(_) | Self::Editable(_) => None,
+ Self::Date { .. } | Self::Editable(_) => None,
}
}
}
@@ -357,8 +357,19 @@ pub(crate) fn build(
})
}
-/// The painted extent of every stroke, pen included, as `[x0, y0, x1, y1]` page points.
+/// The painted extent of a page's ink drawing, pen included, as `[x0, y0, x1, y1]` page
+/// points.
pub(crate) fn ink_bounds(ink: &Ink) -> Option<[f32; 4]> {
+ let [x, y] = ink_offset(ink);
+ stroke_bounds(ink).map(|[x0, y0, x1, y1]| [x0 + x, y0 + y, x1 + x, y1 + y])
+}
+
+pub(crate) fn ink_offset(ink: &Ink) -> [f32; 2] {
+ [ink.layout.x.unwrap_or(0.0), ink.layout.y.unwrap_or(0.0)]
+}
+
+/// The painted extent of every stroke before the drawing's offset.
+fn stroke_bounds(ink: &Ink) -> Option<[f32; 4]> {
ink.strokes
.iter()
.flat_map(|stroke| {
@@ -368,7 +379,7 @@ pub(crate) fn ink_bounds(ink: &Ink) -> Option<[f32; 4]> {
.iter()
.map(move |[x, y]| [x - r, y - r, x + r, y + r])
})
- .chain(ink.groups.iter().filter_map(ink_bounds))
+ .chain(ink.groups.iter().filter_map(stroke_bounds))
.reduce(|a, b| {
[
a[0].min(b[0]),
diff --git a/crates/canvas/src/editor/space.rs b/crates/canvas/src/editor/space.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5e4a9c13aa74998baa34899594c8f3fba80b2d6c
--- /dev/null
+++ b/crates/canvas/src/editor/space.rs
@@ -0,0 +1,336 @@
+//! OneNote 2010's Insert Space: what lies past a line across the page moves along one axis,
+//! and a body outline the line crosses parts between its paragraphs.
+
+use super::*;
+
+/// Something Insert Space moves.
+enum Piece {
+ /// A body outline, picture or ink drawing, placed anew.
+ Object(ExGuid),
+ /// The root nodes of a body outline from `at` on.
+ Tail { outline: ExGuid, at: usize },
+}
+
+impl CanvasEditor {
+ /// What starts at or past `line` along `axis` (0 for x, 1 for y) with its extent as
+ /// `[x0, y0, x1, y1]` page points, and the farthest end of what starts before the line.
+ fn spaced(&self, axis: usize, line: f32) -> (Vec<(Piece, [f32; 4])>, f32) {
+ let mut moved = Vec::new();
+ let mut floor = f32::NEG_INFINITY;
+ let mut stays = |rect: [f32; 4]| {
+ if rect[axis] < line {
+ floor = floor.max(rect[axis + 2]);
+ }
+ };
+ for outline in &self.outlines {
+ let bounds = outline.bounds();
+ let rect = [bounds.x0, bounds.y0, bounds.x1, bounds.y1].map(|value| value as f32);
+ if outline.title {
+ stays(rect);
+ continue;
+ }
+ if rect[axis] >= line {
+ moved.push((Piece::Object(outline.id), rect));
+ continue;
+ }
+ let top = outline.origin()[1];
+ let spans: Vec