diff --git a/corpus/page-model/README.md b/corpus/page-model/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c1307cf93d91157ea6b88a82ede89a3b253f182d --- /dev/null +++ b/corpus/page-model/README.md @@ -0,0 +1,32 @@ +# Page-model edits through a cold native reopen + +`candidate` is `corpus/outline-edit/before` after eight compound `PreparedEdit::page` +publications, one per edited page, each combining several kinds of change in a single +transaction: paragraph reorder and Unicode replacement with a bold range; a subtree +move with a new nested child and an italic/colored/resized range; a subtree deletion +with an appended paragraph and a new two-paragraph outline; a mid-paragraph split; a +collapse with an outline move and fixed width; an outline reorder with text appended +in both; an outline deletion with underline/highlight/font changes; and a paragraph +join with strike/subscript. `candidate/manifest.json` records each case, the page's +position in the section and the model the writer read back. + +`cold` is an independent OneNote 2010 capture of the candidate through a fresh +application cache, with the notebook retained after the native open. `followup` +opens `cold/notebook` in another fresh clone and replaces the Rust-written Unicode +paragraph on "Move leaf down" natively (`tools/native/page-model-followup.ps1`); +`followup-cold` reopens that result cold. + +`PYTHONPATH=tools python3 -m unittest test_page_model -v` compares every page's +native XML with the Rust model of each notebook (text order, resolved formatting, +associated tags), requires the cold reopen to leave every page space's active +revision unchanged (OneNote only adds its per-series navigation metadata copies to +the section root on open, as it does for the untouched `outline-edit` fixture whose +first-page copy was already stale), and checks each case's expected outline positions, paragraph order, +nesting and collapse state in the candidate, the cold notebook and the follow-up. + +To regenerate: `ONESTORE_PAGE_MODEL_OUTPUT=/absolute/new cargo test -p onestore +--test page_writer export_native_page_model_candidates -- --ignored`, then +`tools/native_runner.py /absolute/new/candidate/notebook /absolute/new/cold +--expected-pages 15 --collect-notebook`, then the same runner on `cold/notebook` +with `--author tools/native/page-model-followup.ps1` into `followup`, and a final +cold capture of `followup/notebook` into `followup-cold`. Each runner removes its clone. diff --git a/corpus/page-model/candidate/manifest.json b/corpus/page-model/candidate/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..1c8744c160c47abf5fdbbf6b18f2bc39f57d6526 --- /dev/null +++ b/corpus/page-model/candidate/manifest.json @@ -0,0 +1,1505 @@ +{ + "author": "Rust page writer", + "cases": [ + { + "description": "Anchor moved last and replaced with Unicode text, bold range on Trailing sibling", + "expected": { + "outlines": [ + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": true, + "color": 4278190080, + "end": 12, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 57, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 67, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Target 🦀 é ﷟HYPERLINK \"https://example.invalid/outline\"Link label" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": true, + "color": 4278190080, + "end": 4, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 16, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Trailing sibling" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 17, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Rust 🦀 é 東京 שלום" + } + ], + "width_set_by_user": null, + "x": 72.0, + "y": 108.0 + }, + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Other outline" + } + ], + "width_set_by_user": null, + "x": 480.0, + "y": 108.0 + } + ], + "title": "Move leaf down" + }, + "index": 1, + "space": "{B7267338-BF83-0893-27A7-37F606DD20C8},1", + "title": "Move leaf down" + }, + { + "description": "Target subtree moved last with a new nested child, italic blue 14 pt range on Anchor", + "expected": { + "outlines": [ + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 16711680, + "end": 6, + "font": "Calibri", + "font_size": 14.0, + "highlight": 4278190080, + "italic": true, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Anchor" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 16, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Trailing sibling" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": true, + "color": 4278190080, + "end": 12, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 57, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 67, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Target 🦀 é ﷟HYPERLINK \"https://example.invalid/outline\"Link label" + }, + { + "collapsed": false, + "parent": 2, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 5, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": true, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Child" + }, + { + "collapsed": false, + "parent": 3, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 10, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Grandchild" + }, + { + "collapsed": false, + "parent": 2, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 14, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Nested by Rust" + } + ], + "width_set_by_user": null, + "x": 72.0, + "y": 108.0 + }, + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Other outline" + } + ], + "width_set_by_user": null, + "x": 480.0, + "y": 108.0 + } + ], + "title": "Move subtree down" + }, + "index": 2, + "space": "{9B80D7CA-18B4-02B0-1E85-6E8AA0DAD827},1", + "title": "Move subtree down" + }, + { + "description": "Anchor deleted, paragraph appended, new outline with two paragraphs at (300, 400)", + "expected": { + "outlines": [ + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": true, + "color": 4278190080, + "end": 12, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 57, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 67, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Target 🦀 é ﷟HYPERLINK \"https://example.invalid/outline\"Link label" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 16, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Trailing sibling" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 16, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Appended by Rust" + } + ], + "width_set_by_user": null, + "x": 72.0, + "y": 108.0 + }, + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Other outline" + } + ], + "width_set_by_user": null, + "x": 480.0, + "y": 108.0 + }, + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 16, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Rust outline one" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 16, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Rust outline two" + } + ], + "width_set_by_user": null, + "x": 300.0, + "y": 400.0 + } + ], + "title": "Delete leaf" + }, + "index": 8, + "space": "{363FE055-44E1-0B27-14A8-46278FD681FF},1", + "title": "Delete leaf" + }, + { + "description": "Anchor split in the middle", + "expected": { + "outlines": [ + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 3, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Anc" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 3, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "hor" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": true, + "color": 4278190080, + "end": 12, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 57, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 67, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Target 🦀 é ﷟HYPERLINK \"https://example.invalid/outline\"Link label" + }, + { + "collapsed": false, + "parent": 2, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 5, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": true, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Child" + }, + { + "collapsed": false, + "parent": 3, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 10, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Grandchild" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 16, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Trailing sibling" + } + ], + "width_set_by_user": null, + "x": 72.0, + "y": 108.0 + }, + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Other outline" + } + ], + "width_set_by_user": null, + "x": 480.0, + "y": 108.0 + } + ], + "title": "Delete subtree" + }, + "index": 9, + "space": "{8BBAD88F-D248-0959-00B3-2438A7065330},1", + "title": "Delete subtree" + }, + { + "description": "Target collapsed, outline moved to (90, 250.5) with a fixed 300 pt width", + "expected": { + "outlines": [ + { + "max_width": 300.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 6, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Anchor" + }, + { + "collapsed": true, + "parent": null, + "spans": [ + { + "bold": true, + "color": 4278190080, + "end": 12, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 57, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 67, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Target 🦀 é ﷟HYPERLINK \"https://example.invalid/outline\"Link label" + }, + { + "collapsed": false, + "parent": 1, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 5, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": true, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Child" + }, + { + "collapsed": false, + "parent": 2, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 10, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Grandchild" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 16, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Trailing sibling" + } + ], + "width_set_by_user": true, + "x": 90.0, + "y": 250.5 + }, + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Other outline" + } + ], + "width_set_by_user": null, + "x": 480.0, + "y": 108.0 + } + ], + "title": "Collapse subtree" + }, + "index": 6, + "space": "{FF88B513-029C-00E2-0082-9E3DCE1D7796},1", + "title": "Collapse subtree" + }, + { + "description": "first body outline moved after the second, first paragraph of each extended", + "expected": { + "outlines": [ + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 20, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Other outline (Rust)" + } + ], + "width_set_by_user": null, + "x": 480.0, + "y": 108.0 + }, + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Anchor (Rust)" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": true, + "color": 4278190080, + "end": 12, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 57, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 67, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Target 🦀 é ﷟HYPERLINK \"https://example.invalid/outline\"Link label" + }, + { + "collapsed": false, + "parent": 1, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 5, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": true, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Child" + }, + { + "collapsed": false, + "parent": 2, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 10, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Grandchild" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 16, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Trailing sibling" + } + ], + "width_set_by_user": null, + "x": 72.0, + "y": 108.0 + } + ], + "title": "Move outline" + }, + "index": 12, + "space": "{16207F40-69C9-0CA1-09B5-DC5712DB2BBE},1", + "title": "Move outline" + }, + { + "description": "second body outline deleted, underline, highlight and Consolas on a range of Anchor", + "expected": { + "outlines": [ + { + "max_width": 468.0, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 1, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 5, + "font": "Consolas", + "font_size": 11.0, + "highlight": 65535, + "italic": false, + "strike": false, + "subscript": false, + "underline": true + }, + { + "bold": false, + "color": 4278190080, + "end": 6, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Anchor" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": true, + "color": 4278190080, + "end": 12, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 13, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 57, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": false, + "color": 4278190080, + "end": 67, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Target 🦀 é ﷟HYPERLINK \"https://example.invalid/outline\"Link label" + }, + { + "collapsed": false, + "parent": 1, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 5, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": true, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Child" + }, + { + "collapsed": false, + "parent": 2, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 10, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Grandchild" + }, + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": false, + "color": 4278190080, + "end": 16, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Trailing sibling" + } + ], + "width_set_by_user": null, + "x": 72.0, + "y": 108.0 + } + ], + "title": "Delete outline" + }, + "index": 11, + "space": "{80E80543-87A1-0C3A-0816-22B190BDFC87},1", + "title": "Delete outline" + }, + { + "description": "the two paragraphs joined, strike and subscript on the first three units", + "expected": { + "outlines": [ + { + "max_width": 127.5136947631836, + "paragraphs": [ + { + "collapsed": false, + "parent": null, + "spans": [ + { + "bold": true, + "color": null, + "end": 3, + "font": "Calibri", + "font_size": 11.0, + "highlight": null, + "italic": null, + "strike": true, + "subscript": true, + "underline": true + }, + { + "bold": true, + "color": null, + "end": 4, + "font": "Calibri", + "font_size": 11.0, + "highlight": null, + "italic": null, + "strike": null, + "subscript": null, + "underline": true + }, + { + "bold": null, + "color": null, + "end": 5, + "font": "Calibri", + "font_size": 11.0, + "highlight": null, + "italic": null, + "strike": null, + "subscript": null, + "underline": null + }, + { + "bold": null, + "color": null, + "end": 7, + "font": "Arial", + "font_size": 13.5, + "highlight": null, + "italic": null, + "strike": null, + "subscript": null, + "underline": null + }, + { + "bold": null, + "color": null, + "end": 8, + "font": "Calibri", + "font_size": 11.0, + "highlight": null, + "italic": null, + "strike": null, + "subscript": null, + "underline": null + }, + { + "bold": null, + "color": null, + "end": 14, + "font": "Calibri", + "font_size": 11.0, + "highlight": null, + "italic": true, + "strike": true, + "subscript": null, + "underline": null + }, + { + "bold": null, + "color": null, + "end": 15, + "font": "Calibri", + "font_size": 11.0, + "highlight": null, + "italic": null, + "strike": null, + "subscript": null, + "underline": null + }, + { + "bold": null, + "color": null, + "end": 17, + "font": "Calibri", + "font_size": 11.0, + "highlight": null, + "italic": null, + "strike": null, + "subscript": false, + "underline": null + }, + { + "bold": null, + "color": null, + "end": 18, + "font": "Calibri", + "font_size": 11.0, + "highlight": null, + "italic": null, + "strike": null, + "subscript": null, + "underline": null + }, + { + "bold": null, + "color": 3678732, + "end": 23, + "font": "Calibri", + "font_size": 11.0, + "highlight": 65535, + "italic": null, + "strike": null, + "subscript": null, + "underline": null + }, + { + "bold": null, + "color": null, + "end": 24, + "font": "Calibri", + "font_size": 11.0, + "highlight": null, + "italic": null, + "strike": null, + "subscript": null, + "underline": null + }, + { + "bold": null, + "color": null, + "end": 26, + "font": "Calibri", + "font_size": 11.0, + "highlight": null, + "italic": null, + "strike": null, + "subscript": true, + "underline": null + }, + { + "bold": null, + "color": null, + "end": 27, + "font": "Calibri", + "font_size": 11.0, + "highlight": null, + "italic": null, + "strike": null, + "subscript": null, + "underline": null + }, + { + "bold": false, + "color": 4278190080, + "end": 30, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": null, + "subscript": null, + "underline": null + }, + { + "bold": true, + "color": 7949855, + "end": 48, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": true, + "color": 7949855, + "end": 50, + "font": "SimSun", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": true, + "color": 7949855, + "end": 52, + "font": "Calibri", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + }, + { + "bold": true, + "color": 7949855, + "end": 57, + "font": "Arial", + "font_size": 11.0, + "highlight": 4278190080, + "italic": false, + "strike": false, + "subscript": false, + "underline": false + } + ], + "text": "Bold 🦀 italic é color 東京\rEndFictitious: café, 東京, مرحبا" + } + ], + "width_set_by_user": false, + "x": 36.0, + "y": 14.40000057220459 + } + ], + "title": "Bold 🦀 italic é color 東京" + }, + "index": 0, + "space": "{64183EF4-4E19-0CBE-3D3B-88FF17D58B1C},1", + "title": "Bold 🦀 italic é color 東京" + } + ], + "source": "corpus/outline-edit/before/notebook/synthetic.one", + "transactions_added": 8 +} \ No newline at end of file diff --git a/corpus/page-model/candidate/notebook/Open Notebook.onetoc2 b/corpus/page-model/candidate/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..5814b0f6d1941721eba9f84289aa7d970c3f3e23 Binary files /dev/null and b/corpus/page-model/candidate/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/page-model/candidate/notebook/synthetic.one b/corpus/page-model/candidate/notebook/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..825678cbeddf443be391fa6dfadfab946270690a Binary files /dev/null and b/corpus/page-model/candidate/notebook/synthetic.one differ diff --git a/corpus/page-model/cold/commands.jsonl b/corpus/page-model/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..e5d5865aad2998d7cbf08588ecbad1fa2ea0aa1a --- /dev/null +++ b/corpus/page-model/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-A298124B -ExpectedPages 15", "exit": 0, "stdout": "Read 1 sections and 15 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/page-model/cold/format-differences.json b/corpus/page-model/cold/format-differences.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/corpus/page-model/cold/format-differences.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/corpus/page-model/cold/geometry-differences.json b/corpus/page-model/cold/geometry-differences.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/corpus/page-model/cold/geometry-differences.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/corpus/page-model/cold/machine.json b/corpus/page-model/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..83fbd6e93f48c501e15c7da983294265c5bbf057 --- /dev/null +++ b/corpus/page-model/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-a298124b", "hostname": "ONE-M6-A298124B"} diff --git a/corpus/page-model/cold/notebook/Open Notebook.onetoc2 b/corpus/page-model/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..5814b0f6d1941721eba9f84289aa7d970c3f3e23 Binary files /dev/null and b/corpus/page-model/cold/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/page-model/cold/notebook/synthetic.one b/corpus/page-model/cold/notebook/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..b89cecf3c6a19fc7a582e417142f48b0a0e9bee5 Binary files /dev/null and b/corpus/page-model/cold/notebook/synthetic.one differ diff --git a/corpus/page-model/cold/pdf-format-checks.json b/corpus/page-model/cold/pdf-format-checks.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/corpus/page-model/cold/pdf-format-checks.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/corpus/page-model/cold/read/environment.json b/corpus/page-model/cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..97ef6379034194d40a5e909d2ae0459d9f5e0ba4 --- /dev/null +++ b/corpus/page-model/cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-A298124B", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/page-model/cold/read/hierarchy.xml b/corpus/page-model/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..fd224f1883d975b4ad607981b9f37c861607f77f --- /dev/null +++ b/corpus/page-model/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/page-model/cold/read/page-000.xml b/corpus/page-model/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..cdd8d5a5300c396039a7b9d950b44edb40721ae6 --- /dev/null +++ b/corpus/page-model/cold/read/page-000.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/cold/read/page-001.xml b/corpus/page-model/cold/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..7fae1848bc9bc3fafb1c1a3a1f726b05b02fe717 --- /dev/null +++ b/corpus/page-model/cold/read/page-001.xml @@ -0,0 +1,25 @@ + +Bold 🦀 italic é color 東京
+End
Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/page-model/cold/read/page-002.xml b/corpus/page-model/cold/read/page-002.xml new file mode 100644 index 0000000000000000000000000000000000000000..912f188fc0db194126cd0c4d65b2d0551ba48639 --- /dev/null +++ b/corpus/page-model/cold/read/page-002.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/cold/read/page-003.xml b/corpus/page-model/cold/read/page-003.xml new file mode 100644 index 0000000000000000000000000000000000000000..a93fe21237a05a2554304704e2b36b764d3d6835 --- /dev/null +++ b/corpus/page-model/cold/read/page-003.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/cold/read/page-004.xml b/corpus/page-model/cold/read/page-004.xml new file mode 100644 index 0000000000000000000000000000000000000000..6714ad0c7a2999b248fc433c632ae5a75d9bf0cd --- /dev/null +++ b/corpus/page-model/cold/read/page-004.xml @@ -0,0 +1,4 @@ + +Target 🦀 é Link label]]> diff --git a/corpus/page-model/cold/read/page-005.xml b/corpus/page-model/cold/read/page-005.xml new file mode 100644 index 0000000000000000000000000000000000000000..0bdb2812db741f8c1ae79ea92ee659dd34394748 --- /dev/null +++ b/corpus/page-model/cold/read/page-005.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/cold/read/page-006.xml b/corpus/page-model/cold/read/page-006.xml new file mode 100644 index 0000000000000000000000000000000000000000..78bd080854f4d01bb8f75f8410fa662b25570a9a --- /dev/null +++ b/corpus/page-model/cold/read/page-006.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/cold/read/page-007.xml b/corpus/page-model/cold/read/page-007.xml new file mode 100644 index 0000000000000000000000000000000000000000..7311086900e4065b4c6667d1c885530f2d21f605 --- /dev/null +++ b/corpus/page-model/cold/read/page-007.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/cold/read/page-008.xml b/corpus/page-model/cold/read/page-008.xml new file mode 100644 index 0000000000000000000000000000000000000000..c8c5fc98bd379a36e75692477086c6ce031d0658 --- /dev/null +++ b/corpus/page-model/cold/read/page-008.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/cold/read/page-009.xml b/corpus/page-model/cold/read/page-009.xml new file mode 100644 index 0000000000000000000000000000000000000000..e45763cebeb296bd8ad5ea9ef67d259a6353b3bb --- /dev/null +++ b/corpus/page-model/cold/read/page-009.xml @@ -0,0 +1,8 @@ + +Anchor]]>Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/cold/read/page-010.xml b/corpus/page-model/cold/read/page-010.xml new file mode 100644 index 0000000000000000000000000000000000000000..68204199b681e1994a9229b62f91f7cb25759bcb --- /dev/null +++ b/corpus/page-model/cold/read/page-010.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/cold/read/page-011.xml b/corpus/page-model/cold/read/page-011.xml new file mode 100644 index 0000000000000000000000000000000000000000..f06ecb9b24864e22dba90e1bb2d8c82a064cd28d --- /dev/null +++ b/corpus/page-model/cold/read/page-011.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/cold/read/page-012.xml b/corpus/page-model/cold/read/page-012.xml new file mode 100644 index 0000000000000000000000000000000000000000..f48f4db295b0b9f2a27e6a225a49e9f7fcaad1d0 --- /dev/null +++ b/corpus/page-model/cold/read/page-012.xml @@ -0,0 +1,6 @@ + +Anchor]]>Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/cold/read/page-013.xml b/corpus/page-model/cold/read/page-013.xml new file mode 100644 index 0000000000000000000000000000000000000000..929e4aa8822587464ee2d155d29dce707946ca65 --- /dev/null +++ b/corpus/page-model/cold/read/page-013.xml @@ -0,0 +1,4 @@ + +Target 🦀 é Link label]]> diff --git a/corpus/page-model/cold/read/page-014.xml b/corpus/page-model/cold/read/page-014.xml new file mode 100644 index 0000000000000000000000000000000000000000..b35b364f4fce294fcbdbceb0e3061f31240ae709 --- /dev/null +++ b/corpus/page-model/cold/read/page-014.xml @@ -0,0 +1,7 @@ + +Target 🦀 é Link label]]>Trailing sibling]]>Rust 🦀 é 東京 שלום]]> diff --git a/corpus/page-model/cold/read/payloads.json b/corpus/page-model/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/page-model/cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/page-model/cold/run.json b/corpus/page-model/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..1a3ac72580c55a5fcef640b5cc2644c939e87a84 --- /dev/null +++ b/corpus/page-model/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/page-model/candidate/notebook", + "expected_pages": 15, + "author": null, + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/page-model/cold/scripts/cold.ps1 b/corpus/page-model/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/page-model/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-model/cold/scripts/read.ps1 b/corpus/page-model/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/page-model/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-model/cold/source.json b/corpus/page-model/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..f0ca3ba96036b4221b8c96e54c2a5c3dbb9fa253 --- /dev/null +++ b/corpus/page-model/cold/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 4800, + "sha256": "341894b32b0433a1e8b647431f71e372d344c74e0f87699e45c9235964082551", + "mtime_ns": 1788890614579474155 + }, + { + "path": "synthetic.one", + "bytes": 119720, + "sha256": "a5ca37027575ea012fce7fd4131ffb7c184b65bc32836a3f33556ee5162cdbc3", + "mtime_ns": 1789107784789511093 + } +] diff --git a/corpus/page-model/cold/table-autofit.json b/corpus/page-model/cold/table-autofit.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/corpus/page-model/cold/table-autofit.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/corpus/page-model/cold/teardown.json b/corpus/page-model/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/page-model/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/page-model/followup-cold/commands.jsonl b/corpus/page-model/followup-cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..db68e272ae7b372eef26403f745860d8c3f26268 --- /dev/null +++ b/corpus/page-model/followup-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-3CAF6D8E -ExpectedPages 15", "exit": 0, "stdout": "Read 1 sections and 15 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/page-model/followup-cold/format-differences.json b/corpus/page-model/followup-cold/format-differences.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/corpus/page-model/followup-cold/format-differences.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/corpus/page-model/followup-cold/geometry-differences.json b/corpus/page-model/followup-cold/geometry-differences.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/corpus/page-model/followup-cold/geometry-differences.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/corpus/page-model/followup-cold/machine.json b/corpus/page-model/followup-cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..7d7aed8f1e66d397e38bde8a4f94fbeb8c746158 --- /dev/null +++ b/corpus/page-model/followup-cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-3caf6d8e", "hostname": "ONE-M6-3CAF6D8E"} diff --git a/corpus/page-model/followup-cold/notebook/Open Notebook.onetoc2 b/corpus/page-model/followup-cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..5814b0f6d1941721eba9f84289aa7d970c3f3e23 Binary files /dev/null and b/corpus/page-model/followup-cold/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/page-model/followup-cold/notebook/synthetic.one b/corpus/page-model/followup-cold/notebook/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..866983b63f7a2c0a91045600e6e0090d2e38eff2 Binary files /dev/null and b/corpus/page-model/followup-cold/notebook/synthetic.one differ diff --git a/corpus/page-model/followup-cold/pdf-format-checks.json b/corpus/page-model/followup-cold/pdf-format-checks.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/corpus/page-model/followup-cold/pdf-format-checks.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/corpus/page-model/followup-cold/read/environment.json b/corpus/page-model/followup-cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..43b556f1b4cf42bfd37d85816130c48ee5db9b05 --- /dev/null +++ b/corpus/page-model/followup-cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-3CAF6D8E", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/page-model/followup-cold/read/hierarchy.xml b/corpus/page-model/followup-cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..061a800637b9a23f0ec2f31c7d370113e508d36a --- /dev/null +++ b/corpus/page-model/followup-cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/page-model/followup-cold/read/page-000.xml b/corpus/page-model/followup-cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..9a01dd16894e7ebeaf1e43f1c03559facc2c08f3 --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-000.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup-cold/read/page-001.xml b/corpus/page-model/followup-cold/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..d7874313dae613a9b923edf885038180e0dd2018 --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-001.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup-cold/read/page-002.xml b/corpus/page-model/followup-cold/read/page-002.xml new file mode 100644 index 0000000000000000000000000000000000000000..2cd0a70dd1e44fcc029e030a69e229a2ebb6de1b --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-002.xml @@ -0,0 +1,25 @@ + +Bold 🦀 italic é color 東京
+End
Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/page-model/followup-cold/read/page-003.xml b/corpus/page-model/followup-cold/read/page-003.xml new file mode 100644 index 0000000000000000000000000000000000000000..44d97aca0dc3069c10a2ae4d1f1679449dd5a952 --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-003.xml @@ -0,0 +1,4 @@ + +Target 🦀 é Link label]]> diff --git a/corpus/page-model/followup-cold/read/page-004.xml b/corpus/page-model/followup-cold/read/page-004.xml new file mode 100644 index 0000000000000000000000000000000000000000..6fd3b3e9da17a0f4b51308599ba48cc260741b40 --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-004.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup-cold/read/page-005.xml b/corpus/page-model/followup-cold/read/page-005.xml new file mode 100644 index 0000000000000000000000000000000000000000..ac5281a2286f41e2ff1bc86dfede8de821f1ccae --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-005.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup-cold/read/page-006.xml b/corpus/page-model/followup-cold/read/page-006.xml new file mode 100644 index 0000000000000000000000000000000000000000..9a881e4f7e211e1c4c835760ecd5c7b047fb52c6 --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-006.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup-cold/read/page-007.xml b/corpus/page-model/followup-cold/read/page-007.xml new file mode 100644 index 0000000000000000000000000000000000000000..82eff189219270eef4304b6d3d546b55d15dd3a1 --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-007.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup-cold/read/page-008.xml b/corpus/page-model/followup-cold/read/page-008.xml new file mode 100644 index 0000000000000000000000000000000000000000..bfb0e7521f427bac718632ace1bbb218169ad615 --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-008.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Trailing sibling]]> diff --git a/corpus/page-model/followup-cold/read/page-009.xml b/corpus/page-model/followup-cold/read/page-009.xml new file mode 100644 index 0000000000000000000000000000000000000000..279410c4bba45b5f2fbbb328c3e7467814c650aa --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-009.xml @@ -0,0 +1,4 @@ + +Target 🦀 é Link label]]> diff --git a/corpus/page-model/followup-cold/read/page-010.xml b/corpus/page-model/followup-cold/read/page-010.xml new file mode 100644 index 0000000000000000000000000000000000000000..db64a92f227db5e95bd5f1efcd7e55415d93ac7e --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-010.xml @@ -0,0 +1,6 @@ + +Anchor]]>Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup-cold/read/page-011.xml b/corpus/page-model/followup-cold/read/page-011.xml new file mode 100644 index 0000000000000000000000000000000000000000..be44336100e59aec4bbac0c6bf2896c23c1f2b27 --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-011.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup-cold/read/page-012.xml b/corpus/page-model/followup-cold/read/page-012.xml new file mode 100644 index 0000000000000000000000000000000000000000..8d470632f07479305862ace8681927a05b0564ca --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-012.xml @@ -0,0 +1,8 @@ + +Anchor]]>Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup-cold/read/page-013.xml b/corpus/page-model/followup-cold/read/page-013.xml new file mode 100644 index 0000000000000000000000000000000000000000..7d8611d8ad913e301bdc617ddf11b317941e4193 --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-013.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup-cold/read/page-014.xml b/corpus/page-model/followup-cold/read/page-014.xml new file mode 100644 index 0000000000000000000000000000000000000000..8f8180a6978889becd11f4a2cbe80fdb04905021 --- /dev/null +++ b/corpus/page-model/followup-cold/read/page-014.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup-cold/read/payloads.json b/corpus/page-model/followup-cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/page-model/followup-cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/page-model/followup-cold/run.json b/corpus/page-model/followup-cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..a3f67751d55bbc3fd4854e88663a723ff232bba6 --- /dev/null +++ b/corpus/page-model/followup-cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/page-model/followup/notebook", + "expected_pages": 15, + "author": null, + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/page-model/followup-cold/scripts/cold.ps1 b/corpus/page-model/followup-cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/page-model/followup-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-model/followup-cold/scripts/read.ps1 b/corpus/page-model/followup-cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/page-model/followup-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-model/followup-cold/source.json b/corpus/page-model/followup-cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..eea6760b5ae2ae7b63654d66a2f7f891bbc8782e --- /dev/null +++ b/corpus/page-model/followup-cold/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 4800, + "sha256": "341894b32b0433a1e8b647431f71e372d344c74e0f87699e45c9235964082551", + "mtime_ns": 1789109003103685572 + }, + { + "path": "synthetic.one", + "bytes": 127664, + "sha256": "d3f67a59581e9314cc476ea6f9cdef12bfcbce131335da591c68ae35886cace2", + "mtime_ns": 1789109003103953701 + } +] diff --git a/corpus/page-model/followup-cold/table-autofit.json b/corpus/page-model/followup-cold/table-autofit.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/corpus/page-model/followup-cold/table-autofit.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/corpus/page-model/followup-cold/teardown.json b/corpus/page-model/followup-cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/page-model/followup-cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/page-model/followup/before.xml b/corpus/page-model/followup/before.xml new file mode 100644 index 0000000000000000000000000000000000000000..68aca4c763f483fc8ff7037544deb3fb58cf9f1a --- /dev/null +++ b/corpus/page-model/followup/before.xml @@ -0,0 +1,7 @@ + +Target 🦀 é Link label]]>Trailing sibling]]>Rust 🦀 é 東京 שלום]]> diff --git a/corpus/page-model/followup/commands.jsonl b/corpus/page-model/followup/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..0344ee05bb75f4950c49dbae5380d59594c70dd2 --- /dev/null +++ b/corpus/page-model/followup/commands.jsonl @@ -0,0 +1,4 @@ +{"command": "powershell -NoProfile -Command \"Expand-Archive -LiteralPath C:\\one-tests\\transfer.zip -DestinationPath C:\\one-tests\\runs\\capture\\notebook\"", "exit": 0, "stdout": "", "stderr": "", "error": null} +{"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\author.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-A302D412", "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-A302D412 -ExpectedPages 15 -UseCurrentCache", "exit": 0, "stdout": "Read 1 sections and 15 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/page-model/followup/machine.json b/corpus/page-model/followup/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..6e97d47cc8edf5c9b65c203076631b82c349f6b8 --- /dev/null +++ b/corpus/page-model/followup/machine.json @@ -0,0 +1 @@ +{"name": "m6-a302d412", "hostname": "ONE-M6-A302D412"} diff --git a/corpus/page-model/followup/native-text.txt b/corpus/page-model/followup/native-text.txt new file mode 100644 index 0000000000000000000000000000000000000000..e11d360e254d6f6205718d67be5af1dfa476bc7e --- /dev/null +++ b/corpus/page-model/followup/native-text.txt @@ -0,0 +1 @@ +Native after Rust 🦋 é \ No newline at end of file diff --git a/corpus/page-model/followup/notebook/Open Notebook.onetoc2 b/corpus/page-model/followup/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..5814b0f6d1941721eba9f84289aa7d970c3f3e23 Binary files /dev/null and b/corpus/page-model/followup/notebook/Open Notebook.onetoc2 differ diff --git a/corpus/page-model/followup/notebook/synthetic.one b/corpus/page-model/followup/notebook/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..866983b63f7a2c0a91045600e6e0090d2e38eff2 Binary files /dev/null and b/corpus/page-model/followup/notebook/synthetic.one differ diff --git a/corpus/page-model/followup/read/environment.json b/corpus/page-model/followup/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..5cdb893b75c3763b7d1d48e57820340706abdfab --- /dev/null +++ b/corpus/page-model/followup/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-A302D412", + "cold": false, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/page-model/followup/read/hierarchy.xml b/corpus/page-model/followup/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..b52a9683103c379e8d667177ed785422c7d0e480 --- /dev/null +++ b/corpus/page-model/followup/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/page-model/followup/read/page-000.xml b/corpus/page-model/followup/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..363e762804622eacbb17f591a85b642b2be3fae8 --- /dev/null +++ b/corpus/page-model/followup/read/page-000.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup/read/page-001.xml b/corpus/page-model/followup/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..1a0b4451a204865b60ba37f0cc53eef945d242b1 --- /dev/null +++ b/corpus/page-model/followup/read/page-001.xml @@ -0,0 +1,25 @@ + +Bold 🦀 italic é color 東京
+End
Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/page-model/followup/read/page-002.xml b/corpus/page-model/followup/read/page-002.xml new file mode 100644 index 0000000000000000000000000000000000000000..9389a42efac96f459f0d93e910dd93b4ca4e139d --- /dev/null +++ b/corpus/page-model/followup/read/page-002.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup/read/page-003.xml b/corpus/page-model/followup/read/page-003.xml new file mode 100644 index 0000000000000000000000000000000000000000..33b71f7005065483933c2b8f508f8dda46d00b23 --- /dev/null +++ b/corpus/page-model/followup/read/page-003.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup/read/page-004.xml b/corpus/page-model/followup/read/page-004.xml new file mode 100644 index 0000000000000000000000000000000000000000..6f9775dd4525bad8801ea845aba404d9938222c5 --- /dev/null +++ b/corpus/page-model/followup/read/page-004.xml @@ -0,0 +1,4 @@ + +Target 🦀 é Link label]]> diff --git a/corpus/page-model/followup/read/page-005.xml b/corpus/page-model/followup/read/page-005.xml new file mode 100644 index 0000000000000000000000000000000000000000..021307b398a98489e03a78fd15e5e888dafc1395 --- /dev/null +++ b/corpus/page-model/followup/read/page-005.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup/read/page-006.xml b/corpus/page-model/followup/read/page-006.xml new file mode 100644 index 0000000000000000000000000000000000000000..cf113ac78d4d6711887a19fa0ec8b4ec8c586cf0 --- /dev/null +++ b/corpus/page-model/followup/read/page-006.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup/read/page-007.xml b/corpus/page-model/followup/read/page-007.xml new file mode 100644 index 0000000000000000000000000000000000000000..977329f812d5d008a352e23d4a18c39f30c0195a --- /dev/null +++ b/corpus/page-model/followup/read/page-007.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup/read/page-008.xml b/corpus/page-model/followup/read/page-008.xml new file mode 100644 index 0000000000000000000000000000000000000000..ae9e2068507babfd6fa79e4d6f8b898e39b6fb75 --- /dev/null +++ b/corpus/page-model/followup/read/page-008.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup/read/page-009.xml b/corpus/page-model/followup/read/page-009.xml new file mode 100644 index 0000000000000000000000000000000000000000..b6ba79ceac6a28f81ed5ae44fb8e35877847dcb6 --- /dev/null +++ b/corpus/page-model/followup/read/page-009.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup/read/page-010.xml b/corpus/page-model/followup/read/page-010.xml new file mode 100644 index 0000000000000000000000000000000000000000..dc3dcfb57e76efc3dedf75b7db0b986d374ea962 --- /dev/null +++ b/corpus/page-model/followup/read/page-010.xml @@ -0,0 +1,8 @@ + +Anchor]]>Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup/read/page-011.xml b/corpus/page-model/followup/read/page-011.xml new file mode 100644 index 0000000000000000000000000000000000000000..95ef98b2d062ea6bf46fee0e90319458de3eaf3d --- /dev/null +++ b/corpus/page-model/followup/read/page-011.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup/read/page-012.xml b/corpus/page-model/followup/read/page-012.xml new file mode 100644 index 0000000000000000000000000000000000000000..453ad11a0f0ddb88ed9b013a8cfe84558c1f3b37 --- /dev/null +++ b/corpus/page-model/followup/read/page-012.xml @@ -0,0 +1,6 @@ + +Anchor]]>Target 🦀 é Link label]]>Child]]> diff --git a/corpus/page-model/followup/read/page-013.xml b/corpus/page-model/followup/read/page-013.xml new file mode 100644 index 0000000000000000000000000000000000000000..c2c4b785287053dd76899a6263395a27f08e7634 --- /dev/null +++ b/corpus/page-model/followup/read/page-013.xml @@ -0,0 +1,4 @@ + +Target 🦀 é Link label]]> diff --git a/corpus/page-model/followup/read/page-014.xml b/corpus/page-model/followup/read/page-014.xml new file mode 100644 index 0000000000000000000000000000000000000000..8f855d392b08bddf15a2edadde9ef00211dacde8 --- /dev/null +++ b/corpus/page-model/followup/read/page-014.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Trailing sibling]]> diff --git a/corpus/page-model/followup/read/payloads.json b/corpus/page-model/followup/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/page-model/followup/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/page-model/followup/run.json b/corpus/page-model/followup/run.json new file mode 100644 index 0000000000000000000000000000000000000000..2c2228dcca8537c4d58958caac70e53c1bd67949 --- /dev/null +++ b/corpus/page-model/followup/run.json @@ -0,0 +1,19 @@ +{ + "notebook": "/private/tmp/page-model/cold/notebook", + "expected_pages": 15, + "author": "tools/native/page-model-followup.ps1", + "author_timeout_seconds": 600, + "inspect": false, + "collect_notebook": true, + "base": { + "file": "win7-office-base.qcow2", + "format": "qcow2", + "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", + "virtual_size": 68719476736 + }, + "scripts": { + "author.ps1": "60d2c5b969cafc4e6f066aec2be18323779ea8dceb9bd72473094332895d1219", + "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", + "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" + } +} diff --git a/corpus/page-model/followup/scripts/author.ps1 b/corpus/page-model/followup/scripts/author.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..d688498ef5ab9a3085e693a3a107ada481501e1d --- /dev/null +++ b/corpus/page-model/followup/scripts/author.ps1 @@ -0,0 +1,43 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$app = New-Object -ComObject OneNote.Application +$notebook = '' +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0) + $section = '' + $app.OpenHierarchy('synthetic.one', $notebook, [ref]$section, 0) + $deadline = [DateTime]::UtcNow.AddSeconds(60) + $matches = @() + $content = '' + do { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + $pages = @($tree.SelectNodes('//*[local-name()="Page"]') | Where-Object { $_.GetAttribute('name') -eq 'Move leaf down' }) + if ($pages.Count -eq 1) { + $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1) + [xml]$page = $content + $matches = @($page.SelectNodes('//*[local-name()="T"]') | Where-Object { $_.InnerText -like '*>Rust *' -or $_.InnerText -like 'Rust *' }) + if ($matches.Count -eq 1) { break } + } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if ($matches.Count -ne 1) { throw 'Expected exactly one Rust-written paragraph on the Move leaf down page.' } + [IO.File]::WriteAllText("$Root\before.xml", $content, [Text.Encoding]::UTF8) + $text = 'Native after Rust ' + [char]::ConvertFromUtf32(0x1F98B) + ' e' + [char]0x0301 + $matches[0].InnerText = [Security.SecurityElement]::Escape($text) + $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false) + $app.SyncHierarchy($notebook) + [IO.File]::WriteAllText("$Root\native-text.txt", $text, [Text.Encoding]::UTF8) +} finally { + try { if ($notebook) { $app.CloseNotebook($notebook, $false) } } + finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 120 -ErrorAction SilentlyContinue + } +} diff --git a/corpus/page-model/followup/scripts/cold.ps1 b/corpus/page-model/followup/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/page-model/followup/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-model/followup/scripts/read.ps1 b/corpus/page-model/followup/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/page-model/followup/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-model/followup/source.json b/corpus/page-model/followup/source.json new file mode 100644 index 0000000000000000000000000000000000000000..a1a9c059da619a4e9242f685bfc44eeec98f76ca --- /dev/null +++ b/corpus/page-model/followup/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 4800, + "sha256": "341894b32b0433a1e8b647431f71e372d344c74e0f87699e45c9235964082551", + "mtime_ns": 1789108125813547146 + }, + { + "path": "synthetic.one", + "bytes": 123872, + "sha256": "cb246a4fb88a30dd1dad710ebde9b209a150175fd5dfd3d4e3e36d5d4a505cff", + "mtime_ns": 1789108125813805311 + } +] diff --git a/corpus/page-model/followup/teardown.json b/corpus/page-model/followup/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/page-model/followup/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/page-model/provenance.json b/corpus/page-model/provenance.json new file mode 100644 index 0000000000000000000000000000000000000000..fa132445ac30879709c9395ed5903154e20b2951 --- /dev/null +++ b/corpus/page-model/provenance.json @@ -0,0 +1,37 @@ +{ + "source": { + "path": "corpus/outline-edit/before/notebook/synthetic.one", + "sha256": "924389ae0f4e9fc583fb6de17c0859257994847cbf674bc75b821b3513ca18e2" + }, + "candidate": { + "sha256": "a5ca37027575ea012fce7fd4131ffb7c184b65bc32836a3f33556ee5162cdbc3", + "generator": "cargo test -p onestore --test page_writer export_native_page_model_candidates -- --ignored", + "cases": 8 + }, + "captures": { + "cold": { + "clone": "m6-a298124b", + "teardown_absent": true, + "notebook_sha256": "cb246a4fb88a30dd1dad710ebde9b209a150175fd5dfd3d4e3e36d5d4a505cff", + "pages": 15 + }, + "followup": { + "clone": "m6-a302d412", + "teardown_absent": true, + "notebook_sha256": "d3f67a59581e9314cc476ea6f9cdef12bfcbce131335da591c68ae35886cace2", + "pages": 15 + }, + "followup-cold": { + "clone": "m6-3caf6d8e", + "teardown_absent": true, + "notebook_sha256": "d3f67a59581e9314cc476ea6f9cdef12bfcbce131335da591c68ae35886cace2", + "pages": 15 + } + }, + "author_script": "tools/native/page-model-followup.ps1", + "rejected_attempts": [ + "m6-7b68c992: author script matched no paragraph because native CDATA wraps text in ; matcher widened", + "m6-4c165dd4: native edit saved, script failed waiting 10 s for OneNote to exit; wait made tolerant" + ], + "verifier": "PYTHONPATH=tools python3 -m unittest test_page_model" +} diff --git a/crates/onestore/tests/page_writer.rs b/crates/onestore/tests/page_writer.rs index 73db735cbf78605dba8e8fa75756200f4eaad8fa..69b3e6252676e604e7c7fcd935145b172f77ad7e 100644 --- a/crates/onestore/tests/page_writer.rs +++ b/crates/onestore/tests/page_writer.rs @@ -627,3 +627,361 @@ fn an_unchanged_model_publishes_nothing() { let prepared = PreparedEdit::page(OUTLINES, space, &page, AUTHOR).unwrap(); assert_eq!(prepared.as_bytes(), OUTLINES); } + +const JOIN_PAGE: &str = "Bold 🦀 italic e\u{301} color 東京"; + +fn native_candidate_edits() -> Vec<(&'static str, &'static str)> { + vec![ + ( + "Move leaf down", + "Anchor moved last and replaced with Unicode text, bold range on Trailing sibling", + ), + ( + "Move subtree down", + "Target subtree moved last with a new nested child, italic blue 14 pt range on Anchor", + ), + ( + "Delete leaf", + "Anchor deleted, paragraph appended, new outline with two paragraphs at (300, 400)", + ), + ("Delete subtree", "Anchor split in the middle"), + ( + "Collapse subtree", + "Target collapsed, outline moved to (90, 250.5) with a fixed 300 pt width", + ), + ( + "Move outline", + "first body outline moved after the second, first paragraph of each extended", + ), + ( + "Delete outline", + "second body outline deleted, underline, highlight and Consolas on a range of Anchor", + ), + ( + JOIN_PAGE, + "the two paragraphs joined, strike and subscript on the first three units", + ), + ] +} + +fn fresh(template: &onestore::page::PageParagraph, text: &str) -> onestore::page::PageParagraph { + let mut paragraph = template.clone(); + paragraph.id = new_id().unwrap(); + paragraph.parent = None; + paragraph.level = 1; + paragraph.lists.clear(); + paragraph.tags.clear(); + paragraph.style = None; + paragraph.collapsed = false; + let format = template.text().unwrap().text.format_at(0).unwrap().clone(); + paragraph.content = ParagraphContent::Text(onestore::page::TextObject { + id: new_id().unwrap(), + date_field: None, + text: onestore::page::Paragraph::new(text.into(), format), + tags: Vec::new(), + }); + paragraph +} + +fn restyle( + text: &mut onestore::page::TextObject, + range: std::ops::Range, + change: impl Fn(&mut onestore::document::Format), +) { + let slice = text.text.slice(range.clone()).unwrap(); + let mut runs = Vec::new(); + let mut from = 0; + for span in slice.spans() { + let mut format = span.format.clone(); + change(&mut format); + runs.push((slice.text()[from..span.end].to_owned(), format)); + from = span.end; + } + text.text + .apply(onestore::page::text::Edit { + range, + replacement: onestore::page::Paragraph::from_runs(runs), + }) + .unwrap(); +} + +fn set_text(text: &mut onestore::page::TextObject, replacement: &str) { + let end = text.text.utf16_offset(text.text.text().len()).unwrap(); + let format = text.text.format_at(0).unwrap().clone(); + text.text + .apply(onestore::page::text::Edit { + range: 0..end, + replacement: onestore::page::Paragraph::new(replacement.into(), format), + }) + .unwrap(); +} + +/// Removes a top-level subtree and returns it in order. +fn take_subtree( + outline: &mut onestore::page::Outline, + index: usize, +) -> Vec { + let mut ids = BTreeSet::from([outline.paragraphs[index].id]); + loop { + let before = ids.len(); + let more: Vec = outline + .paragraphs + .iter() + .filter(|p| p.parent.is_some_and(|q| ids.contains(&q))) + .map(|p| p.id) + .collect(); + ids.extend(more); + if ids.len() == before { + break; + } + } + let (subtree, rest): (Vec<_>, Vec<_>) = outline + .paragraphs + .drain(..) + .partition(|p| ids.contains(&p.id)); + outline.paragraphs = rest; + subtree +} + +fn expectation(page: &Page) -> serde_json::Value { + let outlines: Vec = body(page) + .iter() + .map(|outline| { + let index: std::collections::BTreeMap = + outline.paragraphs.iter().enumerate().map(|(i, p)| (p.id, i)).collect(); + serde_json::json!({ + "x": outline.layout.x, + "y": outline.layout.y, + "max_width": outline.layout.max_width, + "width_set_by_user": outline.layout.width_set_by_user, + "paragraphs": outline.paragraphs.iter().map(|p| serde_json::json!({ + "text": p.text().map(|t| t.text.text()), + "parent": p.parent.map(|q| index[&q]), + "collapsed": p.collapsed, + "spans": p.text().map(|t| t.text.spans().iter().map(|s| serde_json::json!({ + "end": t.text.utf16_offset(s.end).unwrap(), + "bold": s.format.bold, "italic": s.format.italic, "underline": s.format.underline, + "strike": s.format.strike, "subscript": s.format.subscript, + "font": s.format.font, "font_size": s.format.font_size, + "color": s.format.color, "highlight": s.format.highlight, + })).collect::>()), + })).collect::>(), + }) + }) + .collect(); + serde_json::json!({ "title": page.title, "outlines": outlines }) +} + +#[test] +#[ignore = "exports a page-model candidate notebook for cold native validation"] +fn export_native_page_model_candidates() { + let output = std::path::PathBuf::from(std::env::var_os("ONESTORE_PAGE_MODEL_OUTPUT").unwrap()); + assert!(output.is_absolute()); + std::fs::create_dir_all(output.join("candidate/notebook")).unwrap(); + let mut bytes = OUTLINES.to_vec(); + let mut cases = Vec::new(); + for (title, description) in native_candidate_edits() { + let (space, _) = page_by_title(&bytes, title); + let mut after = page_in(&bytes, space); + let outlines: Vec = after + .objects + .iter() + .enumerate() + .filter(|(_, o)| matches!(o, PageObject::Outline(_))) + .map(|(i, _)| i) + .collect(); + fn body_outline(page: &mut Page, i: usize) -> &mut onestore::page::Outline { + match &mut page.objects[i] { + PageObject::Outline(outline) => outline, + _ => unreachable!(), + } + } + match title { + "Move leaf down" => { + let outline = body_outline(&mut after, outlines[0]); + let subtree = take_subtree(outline, 0); + outline.paragraphs.extend(subtree); + let last = outline.paragraphs.len() - 1; + set_text( + outline.paragraphs[last].text_mut().unwrap(), + "Rust 🦀 é 東京 שלום", + ); + let trailing = outline + .paragraphs + .iter() + .position(|p| { + p.text() + .is_some_and(|t| t.text.text() == "Trailing sibling") + }) + .unwrap(); + restyle( + outline.paragraphs[trailing].text_mut().unwrap(), + 0..4, + |f| f.bold = Some(true), + ); + } + "Move subtree down" => { + let outline = body_outline(&mut after, outlines[0]); + let parent_index = outline + .paragraphs + .iter() + .position(|p| outline.paragraphs.iter().any(|q| q.parent == Some(p.id))) + .unwrap(); + let parent_id = outline.paragraphs[parent_index].id; + let subtree = take_subtree(outline, parent_index); + outline.paragraphs.extend(subtree); + let mut child = fresh(&outline.paragraphs[0], "Nested by Rust"); + child.parent = Some(parent_id); + child.level = 2; + outline.paragraphs.push(child); + restyle(outline.paragraphs[0].text_mut().unwrap(), 0..6, |f| { + f.italic = Some(true); + f.color = Some(0xff0000); + f.font_size = Some(14.0); + }); + } + "Delete leaf" => { + let template = body(&after)[0].paragraphs[0].clone(); + let outline = body_outline(&mut after, outlines[0]); + take_subtree(outline, 0); + outline + .paragraphs + .push(fresh(&template, "Appended by Rust")); + let new_outline = onestore::page::Outline { + id: new_id().unwrap(), + title: false, + min_width: None, + layout: onestore::document::Layout { + x: Some(300.0), + y: Some(400.0), + ..Default::default() + }, + indents: Vec::new(), + paragraphs: vec![ + fresh(&template, "Rust outline one"), + fresh(&template, "Rust outline two"), + ], + unsupported: Vec::new(), + }; + let at = after + .objects + .iter() + .position(|o| matches!(o, PageObject::Title(_))) + .unwrap_or(after.objects.len()); + after.objects.insert(at, PageObject::Outline(new_outline)); + } + "Delete subtree" => { + let outline = body_outline(&mut after, outlines[0]); + let first = outline.paragraphs[0].clone(); + let text = first.text().unwrap(); + let end = text.text.utf16_offset(text.text.text().len()).unwrap(); + let offset = (end / 2..end) + .find(|at| text.text.byte_offset(*at).is_ok()) + .unwrap(); + let tail = text.text.slice(offset..end).unwrap(); + let mut right = fresh(&first, ""); + right.style = first.style; + right.text_mut().unwrap().text = tail; + let left = outline.paragraphs[0].text_mut().unwrap(); + left.text = left.text.slice(0..offset).unwrap(); + outline.paragraphs.insert(1, right); + } + "Collapse subtree" => { + let outline = body_outline(&mut after, outlines[0]); + let parent = outline + .paragraphs + .iter() + .position(|p| outline.paragraphs.iter().any(|q| q.parent == Some(p.id))) + .unwrap(); + outline.paragraphs[parent].collapsed = true; + outline.layout.x = Some(90.0); + outline.layout.y = Some(250.5); + outline.layout.max_width = Some(300.0); + outline.layout.width_set_by_user = Some(true); + } + "Move outline" => { + assert!(outlines.len() >= 2); + let first = after.objects.remove(outlines[0]); + after.objects.insert(outlines[1], first); + for i in [outlines[0], outlines[1]] { + let outline = body_outline(&mut after, i); + let text = outline.paragraphs[0].text_mut().unwrap(); + let end = text.text.utf16_offset(text.text.text().len()).unwrap(); + let format = text.text.format_at(end).unwrap().clone(); + text.text + .apply(onestore::page::text::Edit { + range: end..end, + replacement: onestore::page::Paragraph::new(" (Rust)".into(), format), + }) + .unwrap(); + } + } + "Delete outline" => { + assert!(outlines.len() >= 2); + after.objects.remove(outlines[1]); + let outline = body_outline(&mut after, outlines[0]); + restyle(outline.paragraphs[0].text_mut().unwrap(), 1..5, |f| { + f.underline = Some(true); + f.highlight = Some(0x00ffff); + f.font = Some("Consolas".into()); + }); + } + _ => { + let outline = body_outline(&mut after, outlines[0]); + assert_eq!(outline.paragraphs.len(), 2); + let right = outline.paragraphs.remove(1); + outline.paragraphs[0] + .text_mut() + .unwrap() + .text + .append(right.text().unwrap().text.clone()) + .unwrap(); + restyle(outline.paragraphs[0].text_mut().unwrap(), 0..3, |f| { + f.strike = Some(true); + f.subscript = Some(true); + }); + } + } + let edit = PreparedEdit::page(&bytes, space, &after, "Rust page writer").unwrap(); + bytes = edit.as_bytes().to_vec(); + let stored = page_in(&bytes, space); + let index = { + let store = Store::parse(&bytes).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + document + .pages() + .unwrap() + .iter() + .position(|(sid, _)| *sid == space) + .unwrap() + }; + cases.push(serde_json::json!({ + "title": title, + "index": index, + "description": description, + "space": space.to_string(), + "expected": expectation(&stored), + })); + } + std::fs::write(output.join("candidate/notebook/synthetic.one"), &bytes).unwrap(); + std::fs::copy( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../corpus/outline-edit/before/notebook/Open Notebook.onetoc2" + ), + output.join("candidate/notebook/Open Notebook.onetoc2"), + ) + .unwrap(); + std::fs::write( + output.join("candidate/manifest.json"), + serde_json::to_string_pretty(&serde_json::json!({ + "source": "corpus/outline-edit/before/notebook/synthetic.one", + "author": "Rust page writer", + "transactions_added": cases.len(), + "cases": cases, + })) + .unwrap(), + ) + .unwrap(); +} diff --git a/tools/native/page-model-followup.ps1 b/tools/native/page-model-followup.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..d688498ef5ab9a3085e693a3a107ada481501e1d --- /dev/null +++ b/tools/native/page-model-followup.ps1 @@ -0,0 +1,43 @@ +param([Parameter(Mandatory=$true)][string]$Root, [Parameter(Mandatory=$true)][string]$CloneHost) +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +& "$PSScriptRoot\cold-current.ps1" -Root $Root -CloneHost $CloneHost +$app = New-Object -ComObject OneNote.Application +$notebook = '' +try { + $app.OpenHierarchy((Join-Path $Root 'notebook'), '', [ref]$notebook, 0) + $section = '' + $app.OpenHierarchy('synthetic.one', $notebook, [ref]$section, 0) + $deadline = [DateTime]::UtcNow.AddSeconds(60) + $matches = @() + $content = '' + do { + $hierarchy = '' + $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) + [xml]$tree = $hierarchy + $pages = @($tree.SelectNodes('//*[local-name()="Page"]') | Where-Object { $_.GetAttribute('name') -eq 'Move leaf down' }) + if ($pages.Count -eq 1) { + $app.GetPageContent($pages[0].GetAttribute('ID'), [ref]$content, 3, 1) + [xml]$page = $content + $matches = @($page.SelectNodes('//*[local-name()="T"]') | Where-Object { $_.InnerText -like '*>Rust *' -or $_.InnerText -like 'Rust *' }) + if ($matches.Count -eq 1) { break } + } + Start-Sleep -Milliseconds 250 + } while ([DateTime]::UtcNow -lt $deadline) + if ($matches.Count -ne 1) { throw 'Expected exactly one Rust-written paragraph on the Move leaf down page.' } + [IO.File]::WriteAllText("$Root\before.xml", $content, [Text.Encoding]::UTF8) + $text = 'Native after Rust ' + [char]::ConvertFromUtf32(0x1F98B) + ' e' + [char]0x0301 + $matches[0].InnerText = [Security.SecurityElement]::Escape($text) + $app.UpdatePageContent($page.OuterXml, [DateTime]::MinValue, 1, $false) + $app.SyncHierarchy($notebook) + [IO.File]::WriteAllText("$Root\native-text.txt", $text, [Text.Encoding]::UTF8) +} finally { + try { if ($notebook) { $app.CloseNotebook($notebook, $false) } } + finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) + $app = $null + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 120 -ErrorAction SilentlyContinue + } +} diff --git a/tools/test_page_model.py b/tools/test_page_model.py new file mode 100644 index 0000000000000000000000000000000000000000..717c335ac3ce0c8d4f6474f00c13b69196b30ab6 --- /dev/null +++ b/tools/test_page_model.py @@ -0,0 +1,133 @@ +"""Rust page-model edits survive a cold native reopen and a native edit on top of them.""" +import json +from pathlib import Path +import runpy +import subprocess +from tempfile import TemporaryDirectory +import unittest + +from document_model import EXPORTER, ordered_pages, walk + +ROOT = Path(__file__).resolve().parent.parent +FIXTURE = ROOT / 'corpus/page-model' +compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] + + +def projection(space, page_root): + """Outlines with their positions and pre-order paragraphs (text, parent index, collapsed).""" + page = space['nodes'][page_root] + outlines = [] + for oid in page['children']: + node = space['nodes'][oid] + if node['kind']['type'] != 'Outline': + continue + paragraphs = [] + index = {} + pending = [(child, None) for child in reversed(node['children'])] + while pending: + pid, parent = pending.pop() + paragraph = space['nodes'][pid] + if paragraph['kind']['type'] == 'OutlineGroup': + pending.extend((child, parent) for child in reversed(paragraph['children'])) + continue + assert paragraph['kind']['type'] == 'Paragraph', paragraph['kind']['type'] + text = None + for cid in paragraph['content']: + content = space['nodes'][cid] + if content['kind']['type'] == 'RichText': + text = content['kind']['text'] + index[pid] = len(paragraphs) + paragraphs.append({'text': text, 'parent': index.get(parent), 'collapsed': paragraph['kind'].get('collapse_state') == 1}) + pending.extend((child, pid) for child in reversed(paragraph['children'])) + layout = node['layout'] + outlines.append({'x': points(layout['x']), 'y': points(layout['y']), 'max_width': points(layout['max_width']), + 'width_set_by_user': layout.get('width_set_by_user'), 'paragraphs': paragraphs}) + return outlines + + +def points(value): + """Single-precision points survive JSON with different digits in Rust and Python.""" + return None if value is None else round(value, 3) + + +def expected_projection(case): + return [{'x': points(o['x']), 'y': points(o['y']), 'max_width': points(o['max_width']), 'width_set_by_user': o['width_set_by_user'], + 'paragraphs': [{'text': p['text'], 'parent': p['parent'], 'collapsed': p['collapsed']} for p in o['paragraphs']]} + for o in case['expected']['outlines']] + + +def model(notebook): + with TemporaryDirectory() as temporary: + subprocess.run([EXPORTER, notebook / 'synthetic.one', Path(temporary) / 'model'], check=True) + return json.loads((Path(temporary) / 'model/document.json').read_text()) + + +class PageModelTest(unittest.TestCase): + def setUp(self): + self.manifest = json.loads((FIXTURE / 'candidate/manifest.json').read_text()) + self.assertEqual(len(self.manifest['cases']), 8) + + def check_cases(self, notebook, exceptions=()): + pages = list(ordered_pages(model(notebook))) + self.assertEqual(len(pages), 15) + for case in self.manifest['cases']: + with self.subTest(case=case['title']): + _, _, space, root = pages[case['index']] + actual = projection(space, root) + expected = expected_projection(case) + for index, replacement in exceptions: + if index == case['index']: + expected = replacement(expected) + self.assertEqual(actual, expected) + + def assert_only_navigation_cache_changed(self, candidate, cold): + """OneNote adds per-series metadata copies on open (as it does for the untouched + outline-edit fixture); every page space must keep its exact active revision.""" + context = '{00000000-0000-0000-0000-000000000000},0' + self.assertEqual(set(candidate['spaces']), set(cold['spaces'])) + for sid, space in candidate['spaces'].items(): + if sid == candidate['root']: + continue + self.assertEqual(space['contexts'][context], cold['spaces'][sid]['contexts'][context], sid) + before = candidate['spaces'][candidate['root']] + after = cold['spaces'][cold['root']] + before = before['revisions'][before['contexts'][context]]['nodes'] + after = after['revisions'][after['contexts'][context]]['nodes'] + self.assertTrue(set(before) <= set(after)) + for oid, node in before.items(): + other = after[oid] + if node['kind']['type'] == 'Metadata': + continue + if node['kind']['type'] == 'PageSeries': + strip = lambda n: {k: v for k, v in n.items() if k != 'extra'} + self.assertEqual(strip(node), strip(other), oid) + continue + self.assertEqual(node, other, oid) + for oid in set(after) - set(before): + self.assertEqual(after[oid]['kind']['type'], 'Metadata', oid) + + def test_rust_page_model_edits_survive_cold_native_reopen(self): + compare(FIXTURE / 'candidate/notebook', FIXTURE / 'cold/read') + compare(FIXTURE / 'cold/notebook', FIXTURE / 'cold/read') + self.assert_only_navigation_cache_changed(model(FIXTURE / 'candidate/notebook'), model(FIXTURE / 'cold/notebook')) + self.check_cases(FIXTURE / 'candidate/notebook') + self.check_cases(FIXTURE / 'cold/notebook') + + def test_native_edit_after_rust_page_model_edits(self): + compare(FIXTURE / 'followup-cold/notebook', FIXTURE / 'followup-cold/read') + native = (FIXTURE / 'followup/native-text.txt').read_text(encoding='utf-8-sig') + self.assertTrue(native.startswith('Native after Rust')) + moved = next(case for case in self.manifest['cases'] if case['title'] == 'Move leaf down') + + def replace_rust_text(outlines): + outlines = json.loads(json.dumps(outlines)) + hits = [p for o in outlines for p in o['paragraphs'] if p['text'].startswith('Rust ')] + assert len(hits) == 1 + hits[0]['text'] = native + return outlines + + self.check_cases(FIXTURE / 'followup-cold/notebook', exceptions=[(moved['index'], replace_rust_text)]) + + +if __name__ == '__main__': + unittest.main()