| author | |
| committer | |
| log | 3d72c72fcc4bf5ea8b86377b6117843fea073b61 |
| tree | 942a226393504ab5a0605abf25ac109664ac9bd6 |
| parent | 75e67fc117387e3301560b6a3ee21075f3487b71 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
`Page::copy` gives a page's content fresh identities (every `{guid},n` in the model, keys
and references alike, becomes `{fresh},1`) with its definitions and payloads, and
`Section::import_page` queues a page creation and a save for it in the destination, so a
copy publishes offline like any edit; `Section::delete_pages` completes a move. Picture
and attachment payloads now serialize with the model (base64), because a queued intent
replayed without them failed publication for any new picture or attachment.
The writer learned what copied pages need: a new outline may start with a picture, table,
attachment or ink paragraph and takes the model's indentation table; a new paragraph in a
new outline may reference a paragraph style, whose object the writer creates from the
definition as OneNote keeps quick styles. A styled paragraph appended to a stored outline
still stays for review, as the split-merge lane expects.
`corpus/page-copy` is the cold read of eight copied pages (text with attachment, table,
picture, ink, tags, lists, equations, paragraph formatting) with screenshots; the
`QuickStyleDef` export carries the copied style's font, size and colour.
`evidence/P01-BASELINE.md` records the queue and large-notebook baselines.
Assisted-by: claude-fable-5.153 files changed, 969 insertions(+), 16 deletions(-)
corpus/page-copy/README.md created+23| ... | ... | @@ -0,0 +1,23 @@ |
| 1 | # Pages copied between sections | |
| 2 | ||
| 3 | `candidate/copies.one` is the destination section | |
| 4 | `pages_copy_into_another_section_with_their_content_and_fresh_identities` in | |
| 5 | `crates/notebook/tests/copy.rs` builds: a section created in Rust into which | |
| 6 | `Section::import_page` copied one page from each of eight earlier rows (text | |
| 7 | with an attachment and its icon, a table, an inserted picture, a page-level | |
| 8 | ink drawing, note tags, bullets and numbering, equations, paragraph | |
| 9 | formatting), each as a page creation and a save queued through the replica | |
| 10 | and published to the file; the last copy (paragraph formatting) was then deleted permanently | |
| 11 | through `Section::delete_pages` (`expected-count.txt` is the page count | |
| 12 | left). Every object carries a fresh identity (`Page::copy`), paragraph | |
| 13 | styles become quick-style objects the writer creates from the copied | |
| 14 | definitions (OneNote exports their font, size and colour through | |
| 15 | `QuickStyleDef`, not on the runs), and payloads travelled inside the queued | |
| 16 | intents. | |
| 17 | ||
| 18 | `cold/` is a fresh OneNote 2010 read with a screenshot per page | |
| 19 | (`read/page-NNN.png`). `tools/test_page_copy.py` checks the content oracle | |
| 20 | and the page set without a VM. Regenerate with `NOTEBOOK_COPY_EXPORT` set to | |
| 21 | a new directory while running the test, then cold-open with | |
| 22 | `tools/native_runner.py OUTPUT COLD --expected-pages 9 --collect-notebook | |
| 23 | --screenshots`. |
corpus/page-copy/candidate/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/page-copy/candidate/Open Notebook.onetoc2 differ |
corpus/page-copy/candidate/copies.one created| Binary files /dev/null and b/corpus/page-copy/candidate/copies.one differ |
corpus/page-copy/candidate/expected-count.txt created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | 9 | |
| \ No newline at end of file |
corpus/page-copy/cold/commands.jsonl created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | {"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} | |
| 2 | {"command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File C:\\one-tests\\read-current.ps1 -Root C:\\one-tests\\runs\\capture -CloneHost ONE-M6-1C0F2CDE -ExpectedPages 9 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 9 pages.\r\n", "stderr": "", "error": null} | |
| 3 | {"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} |
corpus/page-copy/cold/machine.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"name": "m6-1c0f2cde", "hostname": "ONE-M6-1C0F2CDE"} |
corpus/page-copy/cold/notebook/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/page-copy/cold/notebook/Open Notebook.onetoc2 differ |
corpus/page-copy/cold/notebook/copies.one created| Binary files /dev/null and b/corpus/page-copy/cold/notebook/copies.one differ |
corpus/page-copy/cold/notebook/expected-count.txt created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | 9 | |
| \ No newline at end of file |
corpus/page-copy/cold/read/b20a14e044e33eac39b4665e7d79813b8a3672dbbd27ed9d2e4ac866c13ec4b6.attachment created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | Rust wrote this attachment. |
corpus/page-copy/cold/read/environment.json created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | { | |
| 2 | "powershell": "5.1.14409.1005", | |
| 3 | "schema": "xs2010", | |
| 4 | "hostname": "ONE-M6-1C0F2CDE", | |
| 5 | "cold": true, | |
| 6 | "onenote": "14.0.4763.1000" | |
| 7 | } |
corpus/page-copy/cold/read/hierarchy.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Notebook xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" name="notebook" nickname="notebook" ID="{CA49439A-5FEF-43A6-AB60-E8C051A59087}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-09-13T07:34:46.000Z" color="#B49EDE"><one:Section name="copies" ID="{6D7B6C59-2E45-0D8F-286E-4FD72999E63A}{1}{B0}" path="C:\one-tests\runs\capture\notebook\copies.one" lastModifiedTime="2026-09-13T07:34:46.000Z" color="#8AA8E4"><one:Page ID="{8BB0D046-3BC7-0CE9-33B2-04F7D5E9C6FD}{1}{B0}" name="Destination" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:35.000Z" pageLevel="1"/><one:Page ID="{C12C0ECF-DDF8-0A21-11BE-3074429542E0}{1}{B0}" name="Before the file" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:35.000Z" pageLevel="1"/><one:Page ID="{91EAB23E-CBE0-0D18-1A25-23B9CBC39112}{1}{B0}" name="Before the table" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:35.000Z" pageLevel="1"/><one:Page ID="{B5A45771-4A39-05F8-1953-4F2F9B2B33A5}{1}{B0}" name="Before the picture" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1"/><one:Page ID="{B28A26BE-5DF2-0214-1827-B1747FE95ECC}{1}{B0}" name="Beside the drawing" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1"/><one:Page ID="{5C003E48-8717-05C2-096E-6762316BC0A0}{1}{B0}" name="Open task" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1"/><one:Page ID="{C5E005CB-753E-07E2-0C53-CD40FBB64A63}{1}{B0}" name="Bullet item" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1"/><one:Page ID="{5A5E0FDA-1644-0F00-0C45-8D72178B2031}{1}{B0}" name="Equations below" dateTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1"/><one:Page ID="{D4C06049-3C91-0190-0C2E-1BB1EBA49EBA}{1}{B0}" name="A notebook created in Rust." dateTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1"/></one:Section></one:Notebook> |
corpus/page-copy/cold/read/page-000.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "Equations below - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/page-copy/cold/read/page-000.png created| Binary files /dev/null and b/corpus/page-copy/cold/read/page-000.png differ |
corpus/page-copy/cold/read/page-000.xml created+3| ... | ... | @@ -0,0 +1,3 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{5A5E0FDA-1644-0F00-0C45-8D72178B2031}{1}{B0}" name="Equations below" dateTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:QuickStyleDef index="0" name="PageTitle" font="Calibri" fontSize="17.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title showDate="false" showTime="false" lang="en-US"><one:OE author="Copier" lastModifiedBy="Copier" creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{5A5E0FDA-1644-0F00-0C45-8D72178B2031}{15}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Equations below]]></one:T></one:OE></one:Title><one:Outline author="Copier" lastModifiedBy="Copier" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{75AB7537-48D1-0C4C-297D-B4D4CFA87CA2}{1}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="140.9678649902344" height="208.9916534423828"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{D6CEF286-AFD3-0A03-0655-BA825F09BE5C}{1}{B0}" alignment="left"><one:T><![CDATA[Equations below]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{111AB493-1B15-0171-1BEC-C517BF808F15}{1}{B0}" alignment="left" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:msup><mml:mi>x</mml:mi><mml:mn>2</mml:mn></mml:msup><mml:mo>+</mml:mo><mml:msup><mml:mi>y</mml:mi><mml:mn>2</mml:mn></mml:msup><mml:mo>=</mml:mo><mml:msup><mml:mi>z</mml:mi><mml:mn>2</mml:mn></mml:msup></mml:math><![endif]-->]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{E963BDE9-C0E4-02BE-2D54-23989FC58846}{1}{B0}" alignment="left" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:mfrac><mml:mrow><mml:mi>a</mml:mi><mml:mo>+</mml:mo><mml:mi>b</mml:mi></mml:mrow><mml:mrow><mml:mi>c</mml:mi><mml:mo>+</mml:mo><mml:mi>d</mml:mi></mml:mrow></mml:mfrac></mml:math><![endif]-->]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{869A34E1-759D-03DC-1F28-DE83A586BDD6}{1}{B0}" alignment="left" style="font-size:11.0pt"><one:T><![CDATA[<span | |
| 3 | style='font-family:Calibri' lang=en-US>Inline </span><!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML"><mml:mi>&#120572;</mml:mi><mml:mo>+</mml:mo><mml:mi>&#120573;</mml:mi></mml:math><![endif]-->]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{877DD34D-81AC-038C-0477-B3E0645DD63E}{1}{B0}" alignment="left" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:msub><mml:mi>a</mml:mi><mml:mn>1</mml:mn></mml:msub><mml:mo>+</mml:mo><mml:msub><mml:mi>b</mml:mi><mml:mn>2</mml:mn></mml:msub></mml:math><![endif]-->]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{CD4EC150-40D1-074B-1157-F388604EEBE0}{1}{B0}" alignment="left" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:msubsup><mml:mi>x</mml:mi><mml:mi>i</mml:mi><mml:mn>2</mml:mn></mml:msubsup></mml:math><![endif]-->]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{1B908B9B-F5DF-0E47-3861-D55838C5AB28}{1}{B0}" alignment="left" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:mfenced><mml:mrow><mml:mi>a</mml:mi><mml:mo>+</mml:mo><mml:mi>b</mml:mi></mml:mrow></mml:mfenced></mml:math><![endif]-->]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{2A2E6B30-0040-052F-26F1-91BABCAE46B2}{1}{B0}" alignment="left" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:msubsup><mml:mo stretchy="false">∫</mml:mo><mml:mn>0</mml:mn><mml:mn>1</mml:mn></mml:msubsup><mml:mrow><mml:mi>x</mml:mi><mml:mo>&nbsp;</mml:mo><mml:mi>d</mml:mi><mml:mi>x</mml:mi><mml:mo>&nbsp;</mml:mo></mml:mrow></mml:math><![endif]-->]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{3110679E-A6C8-08CA-14E7-D38AABF07619}{1}{B0}" alignment="left" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:munderover><mml:mo stretchy="false">∑</mml:mo><mml:mrow><mml:mi>i</mml:mi><mml:mo>=</mml:mo><mml:mn>1</mml:mn></mml:mrow><mml:mi>n</mml:mi></mml:munderover><mml:mrow><mml:mi>i</mml:mi><mml:mo>&nbsp;</mml:mo></mml:mrow></mml:math><![endif]-->]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{58A7316B-7529-03F0-2F54-20F513A79572}{1}{B0}" alignment="left" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:msup><mml:mi>x</mml:mi><mml:mn>2</mml:mn></mml:msup><mml:mo>+</mml:mo><mml:mn>1</mml:mn></mml:math><![endif]-->]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{BFE82679-7A72-09C5-266D-FBEC819E237D}{1}{B0}" alignment="left" style="font-family:'Cambria Math';font-size:11.0pt"><one:T><![CDATA[<!--[if mathML]><mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block"><mml:mfrac><mml:mrow><mml:mi>a</mml:mi><mml:mo>+</mml:mo><mml:mi>b</mml:mi></mml:mrow><mml:mrow><mml:msup><mml:mi>c</mml:mi><mml:mn>2</mml:mn></mml:msup></mml:mrow></mml:mfrac></mml:math><![endif]-->]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/page-copy/cold/read/page-001.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "Open task - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/page-copy/cold/read/page-001.png created| Binary files /dev/null and b/corpus/page-copy/cold/read/page-001.png differ |
corpus/page-copy/cold/read/page-001.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{5C003E48-8717-05C2-096E-6762316BC0A0}{1}{B0}" name="Open task" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:TagDef index="0" type="0" symbol="3" fontColor="automatic" highlightColor="none" name="Rust task"/><one:TagDef index="1" type="1" symbol="13" fontColor="#FF0000" highlightColor="#FFFF00" name="Important"/><one:QuickStyleDef index="0" name="PageTitle" font="Calibri" fontSize="17.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title showDate="false" showTime="false" lang="en-US"><one:OE author="Copier" lastModifiedBy="Copier" creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{5C003E48-8717-05C2-096E-6762316BC0A0}{15}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Open task]]></one:T></one:OE></one:Title><one:Outline author="Copier" lastModifiedBy="Copier" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{EA96F1EA-D912-043B-3585-EF6DB73F7D60}{1}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="158.2036895751953" height="53.71086883544922"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{2B4A358D-B65A-0B30-167C-A3C772956F2F}{1}{B0}" alignment="left"><one:Tag index="0" completed="false" disabled="false" creationDate="2020-01-02T03:04:05.000Z"/><one:T><![CDATA[Open task]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{30380DA0-EF03-0A04-03AD-6F50C3F095CB}{1}{B0}" alignment="left"><one:Tag index="0" completed="true" disabled="false" creationDate="2020-01-02T03:04:05.000Z" completionDate="2020-01-02T04:05:06.000Z"/><one:T><![CDATA[Completed task]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{26502F9C-1811-0C5C-23BE-395D96FFF797}{1}{B0}" alignment="left"><one:Tag index="0" completed="false" disabled="false" creationDate="2020-01-02T03:04:05.000Z"/><one:Tag index="1" completed="false" disabled="false" creationDate="2020-01-02T03:04:05.000Z"/><one:T><![CDATA[Important open task]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{B17CBFD9-E3D1-0CC8-0837-B615ED83C6E5}{1}{B0}" alignment="left"><one:T><![CDATA[Untagged]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/page-copy/cold/read/page-002.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "Destination - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/page-copy/cold/read/page-002.png created| Binary files /dev/null and b/corpus/page-copy/cold/read/page-002.png differ |
corpus/page-copy/cold/read/page-002.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{8BB0D046-3BC7-0CE9-33B2-04F7D5E9C6FD}{1}{B0}" name="Destination" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:35.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Outline author="Author" lastModifiedBy="Author" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{6D7B6C59-2E45-0D8F-286E-4FD72999E63A}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="118.3393707275391" height="13.4277172088623"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{6D7B6C59-2E45-0D8F-286E-4FD72999E63A}{24}{B0}" alignment="left"><one:T><![CDATA[Destination]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/page-copy/cold/read/page-003.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "Before the table - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/page-copy/cold/read/page-003.png created| Binary files /dev/null and b/corpus/page-copy/cold/read/page-003.png differ |
corpus/page-copy/cold/read/page-003.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{91EAB23E-CBE0-0D18-1A25-23B9CBC39112}{1}{B0}" name="Before the table" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:35.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:QuickStyleDef index="0" name="PageTitle" font="Calibri" fontSize="17.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title showDate="false" showTime="false" lang="en-US"><one:OE author="Copier" lastModifiedBy="Copier" creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{91EAB23E-CBE0-0D18-1A25-23B9CBC39112}{15}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Before the table]]></one:T></one:OE></one:Title><one:Outline author="Copier" lastModifiedBy="Copier" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{899FA9AA-6E30-05C6-0039-C1174852FBAA}{1}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="395.1299438476562" height="67.21086883544922"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{03DB4753-6DD9-0476-0A44-807F1E239611}{1}{B0}" alignment="left"><one:T><![CDATA[Before the table]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{CC36F460-50AA-0CAC-185C-B2DD3405EC23}{1}{B0}" alignment="left"><one:Table bordersVisible="true" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{F406EA96-EABF-0750-358B-13B077B8877C}{1}{B0}"><one:Columns><one:Column index="0" width="144.0" isLocked="true"/><one:Column index="1" width="216.0" isLocked="true"/></one:Columns><one:Row objectID="{B57E3F32-D364-012F-129A-7A8AFA0A8DE1}{1}{B0}" lastModifiedTime="2026-09-13T07:33:14.000Z"><one:Cell lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{8B99B904-69FB-0ABF-25D8-6EDED1F41EED}{1}{B0}"><one:OEChildren><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{2F0315A5-D396-09FD-0772-968EDD6113D6}{1}{B0}" alignment="left"><one:T><![CDATA[Name]]></one:T></one:OE></one:OEChildren></one:Cell><one:Cell lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{58E45041-62CE-0EFA-3F22-C4509482E3BF}{1}{B0}"><one:OEChildren><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{0F92F8F0-F5F6-0CCA-3E1C-10A217EA11EF}{1}{B0}" alignment="left"><one:T><![CDATA[Value]]></one:T></one:OE></one:OEChildren></one:Cell></one:Row><one:Row objectID="{6D9AA72E-D225-0D46-3D00-5B12D99343C3}{1}{B0}" lastModifiedTime="2026-09-13T07:33:14.000Z"><one:Cell lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{2555B7CF-F12A-013D-0652-4CA9AEF5284C}{1}{B0}"><one:OEChildren><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{A0E15DEA-0A8D-0F42-12FA-36CC54EA289A}{1}{B0}" alignment="left"><one:T><![CDATA[Rust &#129408;]]></one:T></one:OE></one:OEChildren></one:Cell><one:Cell lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{7ED49F5B-8C93-0208-16B6-8816DB6B4151}{1}{B0}"><one:OEChildren><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{D60ED3D2-2081-02A2-1EC5-A3D1190448EB}{1}{B0}" alignment="left"><one:T><![CDATA[Written natively]]></one:T></one:OE></one:OEChildren></one:Cell></one:Row></one:Table></one:OE><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{F18DD402-3CA3-01AB-32DC-D1D5A8545B7F}{1}{B0}" alignment="left"><one:T><![CDATA[After the table]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/page-copy/cold/read/page-004.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "Beside the drawing - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/page-copy/cold/read/page-004.png created| Binary files /dev/null and b/corpus/page-copy/cold/read/page-004.png differ |
corpus/page-copy/cold/read/page-004.xml created+7| ... | ... | @@ -0,0 +1,7 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{B28A26BE-5DF2-0214-1827-B1747FE95ECC}{1}{B0}" name="Beside the drawing" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:QuickStyleDef index="0" name="PageTitle" font="Calibri" fontSize="17.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title showDate="false" showTime="false" lang="en-US"><one:OE author="Copier" lastModifiedBy="Copier" creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{B28A26BE-5DF2-0214-1827-B1747FE95ECC}{15}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Beside the drawing]]></one:T></one:OE></one:Title><one:Outline author="Copier" lastModifiedBy="Copier" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{BA59696B-EAE1-0771-0BCB-5878F45A65F8}{1}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="152.4244079589843" height="13.4277172088623"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{38FEF76B-56AE-0456-1B54-A2C668C24685}{1}{B0}" alignment="left"><one:T><![CDATA[Beside the drawing]]></one:T></one:OE></one:OEChildren></one:Outline><one:InkDrawing lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{501D4164-CA81-04D3-1BDC-D2CA0006F088}{1}{B0}"><one:Position x="299.9905395507812" y="119.9905471801758" z="1"/><one:Size width="60.03781127929687" height="60.03779220581055"/><one:Data>AM8CHQSiAaIBAWAqABofi0JwRYR1gL7QRdkE+oeVMvLsF0G4wBa/8yv0et67E4vlXrNInJuhJVq3 | |
| 3 | URK/7HHMzCKxQYkOuxMs7nqhWM9UiuaXxU+PBvi60uGbIsRmbacNslhJmh1OQDQGbBACEQZIEEUj | |
| 4 | RiMJSBBE/wFFI0YjBQgLZU9mZ2hkaRkUMggAgB4CIeLiQTMIAPAQAovF4kFkAxVGAEAKXAWH8gln | |
| 5 | jCj7wnvCe8IAh/B8h4Pke8I7wnvCQAUURgBAAAAJBAUURgAAAAASABEgKFNtArpWcU+AsrjC8JNY | |
| 6 | RQo/QCNkARXUNSA7oAMVRgBACj9AIm9GCC70WAqACQEKTAIPUq7GcA5CJYzgBRRGAIAAAAkEBRRG | |
| 7 | AAAAABIAESAJY0kvgp5dSp0WwhnX6NwFCj9AI2QBFdQ1IDugAxVGAEAKP0Aib0YILvRYCoA=</one:Data></one:InkDrawing></one:Page> |
corpus/page-copy/cold/read/page-005.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "Before the picture - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/page-copy/cold/read/page-005.png created| Binary files /dev/null and b/corpus/page-copy/cold/read/page-005.png differ |
corpus/page-copy/cold/read/page-005.xml created+4| ... | ... | @@ -0,0 +1,4 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{B5A45771-4A39-05F8-1953-4F2F9B2B33A5}{1}{B0}" name="Before the picture" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1" lang="en-US"><one:QuickStyleDef index="0" name="PageTitle" font="Calibri" fontSize="17.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title showDate="false" showTime="false" lang="en-US"><one:OE author="Copier" lastModifiedBy="Copier" creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{B5A45771-4A39-05F8-1953-4F2F9B2B33A5}{15}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Before the picture]]></one:T></one:OE></one:Title><one:Outline author="Copier" lastModifiedBy="Copier" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{6323EB68-A315-07CD-37C1-E46DAF4BAB42}{1}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="148.3100738525391" height="27.60543251037598"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{078AE9E1-0253-0D93-182A-750B047E4D60}{1}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[Before the picture]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{4DBAC23A-2DBC-0548-3811-E4D1B951886F}{1}{B0}" alignment="left"><one:Image format="png"><one:Data>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4z8AAAAMBAQAY3Y2w | |
| 3 | AAAAAElFTkSuQmCC | |
| 4 | </one:Data></one:Image></one:OE><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{79D39D0E-8F58-05A6-1C44-0CB6265FFE97}{1}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[After the picture]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/page-copy/cold/read/page-006.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "Before the file - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/page-copy/cold/read/page-006.png created| Binary files /dev/null and b/corpus/page-copy/cold/read/page-006.png differ |
corpus/page-copy/cold/read/page-006.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{C12C0ECF-DDF8-0A21-11BE-3074429542E0}{1}{B0}" name="Before the file" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:35.000Z" pageLevel="1" lang="en-US"><one:QuickStyleDef index="0" name="PageTitle" font="Calibri" fontSize="17.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title showDate="false" showTime="false" lang="en-US"><one:OE author="Copier" lastModifiedBy="Copier" creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{C12C0ECF-DDF8-0A21-11BE-3074429542E0}{15}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Before the file]]></one:T></one:OE></one:Title><one:Outline author="Copier" lastModifiedBy="Copier" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{A4D24969-1B13-0F34-3174-834AD8FF8832}{1}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="130.4619598388672" height="89.85542297363281"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{6D8ADE9E-3EE4-03A9-2EC9-0A80C321B126}{1}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[Before the file]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{03AD9394-425B-0B9C-0D27-CA384DFFCEBF}{1}{B0}" alignment="left"><one:InsertedFile pathCache="C:\Users\clover\AppData\Local\Microsoft\OneNote\14.0\OneNoteOfflineCache_Files\6d823ba5-6b7e-4df3-947b-a1047440dfa2.txt" preferredName="notes 🦀.txt"/></one:OE><one:OE creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{A6E026B4-3D17-078E-20A3-50C86A85A2AB}{1}{B0}" alignment="left" style="font-family:Calibri;font-size:11.0pt"><one:T><![CDATA[After the file]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/page-copy/cold/read/page-007.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "Bullet item - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/page-copy/cold/read/page-007.png created| Binary files /dev/null and b/corpus/page-copy/cold/read/page-007.png differ |
corpus/page-copy/cold/read/page-007.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{C5E005CB-753E-07E2-0C53-CD40FBB64A63}{1}{B0}" name="Bullet item" dateTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:QuickStyleDef index="0" name="PageTitle" font="Calibri" fontSize="17.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title showDate="false" showTime="false" lang="en-US"><one:OE author="Copier" lastModifiedBy="Copier" creationTime="2026-09-13T07:33:14.000Z" lastModifiedTime="2026-09-13T07:33:14.000Z" objectID="{C5E005CB-753E-07E2-0C53-CD40FBB64A63}{15}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[Bullet item]]></one:T></one:OE></one:Title><one:Outline author="Copier" lastModifiedBy="Copier" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{04550845-DFD8-0D15-199D-14BBB6068FC4}{1}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="174.7353515625" height="93.99401092529296"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{FADAD1EB-13D4-0679-1470-A27D6E3C4C82}{1}{B0}" alignment="left"><one:List><one:Bullet bullet="3" fontSize="11.0"/></one:List><one:T><![CDATA[Bullet item]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{D1386D56-B57F-01DD-3DCE-AE2F7E41CF7B}{1}{B0}" alignment="left"><one:List><one:Number numberSequence="1" numberFormat="##." fontSize="11.0" font="Calibri" text="I."/></one:List><one:T><![CDATA[First numbered]]></one:T></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{8F5A5D13-ADD9-0514-013E-D1A0CBA1E68F}{1}{B0}" alignment="left"><one:List><one:Number numberSequence="1" numberFormat="##." fontSize="11.0" font="Calibri" text="II."/></one:List><one:T><![CDATA[Second numbered]]></one:T><one:OEChildren><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{448444D9-663C-0454-150E-544F2D29DAED}{1}{B0}" alignment="left"><one:List><one:Number numberSequence="1" numberFormat="##." fontSize="11.0" font="Calibri" text="I."/></one:List><one:T><![CDATA[Nested numbered]]></one:T></one:OE></one:OEChildren></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{AB4C6543-D5B5-02F2-18CD-1EA3C75D0B5F}{1}{B0}" alignment="left"><one:List><one:Number numberSequence="1" numberFormat="##." fontSize="11.0" font="Calibri" text="III."/></one:List><one:T><![CDATA[Restarted at three]]></one:T><one:OEChildren><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{1E15DFFF-2CEC-03FA-0FEF-75E2303FB336}{1}{B0}" alignment="left"><one:T><![CDATA[Nested plain]]></one:T></one:OE></one:OEChildren></one:OE><one:OE creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{49D667B4-861E-0253-29DB-8BA25CF21B9E}{1}{B0}" alignment="left"><one:T><![CDATA[Plain again]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/page-copy/cold/read/page-008.navigation.json created+13| ... | ... | @@ -0,0 +1,13 @@ |
| 1 | { | |
| 2 | "exit": 0, | |
| 3 | "stdout": "", | |
| 4 | "stderr": "", | |
| 5 | "w": 800, | |
| 6 | "h": 600, | |
| 7 | "error": null, | |
| 8 | "win": { | |
| 9 | "title": "A notebook created in Rust. - Microsoft OneNote", | |
| 10 | "class": "Framework::CFrame", | |
| 11 | "dialog": false | |
| 12 | } | |
| 13 | } | |
| \ No newline at end of file |
corpus/page-copy/cold/read/page-008.png created| Binary files /dev/null and b/corpus/page-copy/cold/read/page-008.png differ |
corpus/page-copy/cold/read/page-008.xml created+2| ... | ... | @@ -0,0 +1,2 @@ |
| 1 | <?xml version="1.0"?> | |
| 2 | <one:Page xmlns:one="http://schemas.microsoft.com/office/onenote/2010/onenote" ID="{D4C06049-3C91-0190-0C2E-1BB1EBA49EBA}{1}{B0}" name="A notebook created in Rust." dateTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:34:36.000Z" pageLevel="1" style="font-family:Calibri;font-size:11.0pt" lang="en-US"><one:QuickStyleDef index="0" name="PageTitle" font="Calibri" fontSize="17.0"/><one:PageSettings RTL="false" color="automatic"><one:PageSize><one:Automatic/></one:PageSize><one:RuleLines visible="false"/></one:PageSettings><one:Title showDate="false" showTime="false" lang="en-US"><one:OE author="Copier" lastModifiedBy="Copier" creationTime="2026-09-13T07:33:15.000Z" lastModifiedTime="2026-09-13T07:33:15.000Z" objectID="{D4C06049-3C91-0190-0C2E-1BB1EBA49EBA}{15}{B0}" alignment="left" quickStyleIndex="0" style="font-family:Calibri;font-size:17.0pt"><one:T><![CDATA[A notebook created in Rust.]]></one:T></one:OE></one:Title><one:Outline author="Copier" lastModifiedBy="Copier" lastModifiedTime="2026-09-13T07:33:16.000Z" objectID="{F0CA7185-BDBF-066E-0D2C-CAE46E036850}{1}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="191.1661224365234" height="13.4277172088623"/><one:Indents><one:Indent level="0" indent="0.0"/></one:Indents><one:OEChildren><one:OE creationTime="2026-09-13T07:33:16.000Z" lastModifiedTime="2026-09-13T07:33:16.000Z" objectID="{F7D0FEC9-71DC-00E3-30D7-07DB00C2623B}{1}{B0}" alignment="left"><one:T><![CDATA[A notebook created in Rust.]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> |
corpus/page-copy/cold/read/payloads.json created+10| ... | ... | @@ -0,0 +1,10 @@ |
| 1 | [ | |
| 2 | { | |
| 3 | "sha256": "b20a14e044e33eac39b4665e7d79813b8a3672dbbd27ed9d2e4ac866c13ec4b6", | |
| 4 | "name": "notes 🦀.txt", | |
| 5 | "object": "{03AD9394-425B-0B9C-0D27-CA384DFFCEBF}{1}{B0}", | |
| 6 | "kind": "InsertedFile", | |
| 7 | "page": "{C12C0ECF-DDF8-0A21-11BE-3074429542E0}{1}{B0}", | |
| 8 | "bytes": 28 | |
| 9 | } | |
| 10 | ] | |
| \ No newline at end of file |
corpus/page-copy/cold/run.json created+18| ... | ... | @@ -0,0 +1,18 @@ |
| 1 | { | |
| 2 | "notebook": "/private/tmp/m4/copy", | |
| 3 | "expected_pages": 9, | |
| 4 | "author": null, | |
| 5 | "author_timeout_seconds": 600, | |
| 6 | "inspect": false, | |
| 7 | "collect_notebook": true, | |
| 8 | "base": { | |
| 9 | "file": "win7-office-base.qcow2", | |
| 10 | "format": "qcow2", | |
| 11 | "sha256": "a1a4f8fab782ee14885ff801ca2f6347c208fdcfc3513637096f314315c89346", | |
| 12 | "virtual_size": 68719476736 | |
| 13 | }, | |
| 14 | "scripts": { | |
| 15 | "cold.ps1": "c177fc72ae6c2634d186f5671a880de20b09f9716532c37c69cb13aa15c7e331", | |
| 16 | "read.ps1": "04013bfcccee40a17e2a225f9b9e96f40a3a8daad9e350609654f8eda3ccbb41" | |
| 17 | } | |
| 18 | } |
corpus/page-copy/cold/scripts/cold.ps1 created+27| ... | ... | @@ -0,0 +1,27 @@ |
| 1 | param([Parameter(Mandatory=$true)][string]$Root, [string]$CloneHost = '') | |
| 2 | Set-StrictMode -Version Latest | |
| 3 | $ErrorActionPreference = 'Stop' | |
| 4 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 5 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 6 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 7 | } | |
| 8 | if (Get-Process ONENOTE -ErrorAction SilentlyContinue) { throw 'Close OneNote before resetting its test cache.' } | |
| 9 | $key = 'HKCU:\Software\Microsoft\Office\14.0\OneNote' | |
| 10 | if ($CloneHost) { | |
| 11 | if ($CloneHost -notmatch '^ONE-[A-Z0-9-]+$' -or [Environment]::MachineName -ne $CloneHost) { | |
| 12 | throw 'The disposable clone hostname does not match this machine.' | |
| 13 | } | |
| 14 | New-Item "$key\Options\Paths" -Force | Out-Null | |
| 15 | New-ItemProperty "$key\Options\Paths" -Name UnfiledNotesSection -PropertyType ExpandString -Value 'C:\one-tests\Loose.one' -Force | Out-Null | |
| 16 | } elseif ((Get-ItemProperty "$key\Options\Paths").UnfiledNotesSection -ne 'C:\one-tests\Loose.one' -or | |
| 17 | -not (Test-Path 'C:\one-tests\profile-original-cache')) { | |
| 18 | throw 'Park the personal OneNote profile before resetting the test cache.' | |
| 19 | } | |
| 20 | $cache = Join-Path $env:LOCALAPPDATA 'Microsoft\OneNote\14.0' | |
| 21 | $parked = Join-Path 'C:\one-tests\caches' ([IO.Path]::GetFileName($root)) | |
| 22 | if (Test-Path $parked) { throw 'Choose a new run; its parked cache already exists.' } | |
| 23 | New-Item -ItemType Directory -Path 'C:\one-tests\caches' -Force | Out-Null | |
| 24 | if (Test-Path $cache) { Move-Item -LiteralPath $cache -Destination $parked } | |
| 25 | if (Test-Path "$key\OpenNotebooks") { Remove-Item "$key\OpenNotebooks" -Recurse } | |
| 26 | New-Item "$key\OpenNotebooks" | Out-Null | |
| 27 | New-ItemProperty "$key\OpenNotebooks" -Name '1' -PropertyType String -Value "$root\notebook" | Out-Null |
corpus/page-copy/cold/scripts/read.ps1 created+142| ... | ... | @@ -0,0 +1,142 @@ |
| 1 | param( | |
| 2 | [Parameter(Mandatory=$true)][string]$Root, | |
| 3 | [int]$ExpectedPages = -1, | |
| 4 | [switch]$UseCurrentCache, | |
| 5 | [switch]$Pdf, | |
| 6 | [switch]$KeepOpen, | |
| 7 | [string]$CloneHost = '' | |
| 8 | ) | |
| 9 | Set-StrictMode -Version Latest | |
| 10 | $ErrorActionPreference = 'Stop' | |
| 11 | $root = [IO.Path]::GetFullPath($Root).TrimEnd('\') | |
| 12 | if ([IO.Path]::GetDirectoryName($root) -ne 'C:\one-tests\runs') { | |
| 13 | throw 'Choose a run directly below C:\one-tests\runs.' | |
| 14 | } | |
| 15 | $notebook = Join-Path $root 'notebook' | |
| 16 | $output = Join-Path $root 'read' | |
| 17 | if (Test-Path $output) { throw 'Choose a new read destination.' } | |
| 18 | if ($UseCurrentCache) { | |
| 19 | if ((Get-ItemProperty 'HKCU:\Software\Microsoft\Office\14.0\OneNote\Options\Paths').UnfiledNotesSection -ne 'C:\one-tests\Loose.one') { | |
| 20 | throw 'Park the personal OneNote profile before reading test notebooks.' | |
| 21 | } | |
| 22 | } else { | |
| 23 | & "$PSScriptRoot\cold-current.ps1" -Root $root -CloneHost $CloneHost | |
| 24 | } | |
| 25 | New-Item -ItemType Directory -Path $output | Out-Null | |
| 26 | $app = New-Object -ComObject OneNote.Application | |
| 27 | $notebookId = '' | |
| 28 | $failure = $null | |
| 29 | try { | |
| 30 | $app.OpenHierarchy($notebook, '', [ref]$notebookId, 0) | |
| 31 | $process = Get-Process ONENOTE | |
| 32 | @{ hostname = [Environment]::MachineName; onenote = $process.MainModule.FileVersionInfo.FileVersion; | |
| 33 | powershell = $PSVersionTable.PSVersion.ToString(); schema = 'xs2010'; cold = (-not $UseCurrentCache.IsPresent) } | | |
| 34 | ConvertTo-Json | Set-Content (Join-Path $output 'environment.json') -Encoding UTF8 | |
| 35 | $sections = @() | |
| 36 | foreach ($file in @(Get-ChildItem $notebook -Recurse | Where-Object { $_.Extension -eq '.one' })) { | |
| 37 | $id = '' | |
| 38 | $app.OpenHierarchy($file.FullName, '', [ref]$id, 0) | |
| 39 | $sections += $id | |
| 40 | } | |
| 41 | $deadline = [DateTime]::UtcNow.AddSeconds(300) | |
| 42 | $previous = '' | |
| 43 | $lastChange = '' | |
| 44 | $stableSince = [DateTime]::UtcNow | |
| 45 | $settled = $false | |
| 46 | do { | |
| 47 | $pages = @{} | |
| 48 | foreach ($section in $sections) { | |
| 49 | $hierarchy = '' | |
| 50 | $app.GetHierarchy($section, 4, [ref]$hierarchy, 1) | |
| 51 | [xml]$xml = $hierarchy | |
| 52 | foreach ($node in $xml.SelectNodes('//*[@path]')) { | |
| 53 | if (-not $node.GetAttribute('path').StartsWith("$notebook\", [StringComparison]::OrdinalIgnoreCase)) { | |
| 54 | throw 'OneNote opened a section outside the copied notebook.' | |
| 55 | } | |
| 56 | } | |
| 57 | foreach ($node in $xml.SelectNodes('//*[local-name()="Page"]')) { | |
| 58 | $id = $node.GetAttribute('ID') | |
| 59 | $content = '' | |
| 60 | $app.GetPageContent($id, [ref]$content, 1, 1) | |
| 61 | $pages[$id] = $content | |
| 62 | } | |
| 63 | } | |
| 64 | $signature = [String]::Join('|', @($pages.Keys | Sort-Object | ForEach-Object { $_ + $pages[$_] })) | |
| 65 | if ($signature -ne $previous) { | |
| 66 | $lastChange = $previous | |
| 67 | $previous = $signature | |
| 68 | $stableSince = [DateTime]::UtcNow | |
| 69 | } | |
| 70 | if ((($ExpectedPages -ge 0 -and $pages.Count -eq $ExpectedPages) -or | |
| 71 | ($ExpectedPages -lt 0 -and $pages.Count -gt 0)) -and | |
| 72 | ([DateTime]::UtcNow - $stableSince).TotalSeconds -ge 2) { $settled = $true; break } | |
| 73 | Start-Sleep -Milliseconds 250 | |
| 74 | } while ([DateTime]::UtcNow -lt $deadline) | |
| 75 | if (-not $settled -or ($ExpectedPages -ge 0 -and $pages.Count -ne $ExpectedPages) -or ($ExpectedPages -lt 0 -and $pages.Count -eq 0)) { | |
| 76 | [IO.File]::WriteAllText((Join-Path $output 'previous-signature.txt'), $lastChange, [Text.Encoding]::UTF8) | |
| 77 | $index = 0 | |
| 78 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 79 | [IO.File]::WriteAllText((Join-Path $output ('unsettled-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 80 | $index++ | |
| 81 | } | |
| 82 | $hierarchy = '' | |
| 83 | $app.GetHierarchy($notebookId, 4, [ref]$hierarchy, 1) | |
| 84 | [IO.File]::WriteAllText((Join-Path $output 'unsettled-hierarchy.xml'), $hierarchy, [Text.Encoding]::UTF8) | |
| 85 | throw "Expected $ExpectedPages stable pages; OneNote returned $($pages.Count), settled=$settled." | |
| 86 | } | |
| 87 | $index = 0 | |
| 88 | $payloads = @() | |
| 89 | foreach ($id in @($pages.Keys | Sort-Object)) { | |
| 90 | [IO.File]::WriteAllText((Join-Path $output ('page-{0:d3}.xml' -f $index)), $pages[$id], [Text.Encoding]::UTF8) | |
| 91 | if ($Pdf) { | |
| 92 | $pdfPath = Join-Path $output ('page-{0:d3}.pdf' -f $index) | |
| 93 | $app.NavigateTo($id, '', $false) | |
| 94 | $app.Publish($id, $pdfPath, 3, '') | |
| 95 | if (-not (Test-Path $pdfPath) -or (Get-Item $pdfPath).Length -eq 0) { throw 'OneNote did not publish the page PDF.' } | |
| 96 | } | |
| 97 | [xml]$page = $pages[$id] | |
| 98 | foreach ($file in $page.SelectNodes('//*[local-name()="InsertedFile" or local-name()="MediaFile"]')) { | |
| 99 | $bytes = [IO.File]::ReadAllBytes($file.GetAttribute('pathCache')) | |
| 100 | $hash = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace('-', '').ToLowerInvariant() | |
| 101 | [IO.File]::WriteAllBytes((Join-Path $output ($hash + '.attachment')), $bytes) | |
| 102 | $payloads += @{ page = $id; object = $file.ParentNode.GetAttribute('objectID'); | |
| 103 | kind = $file.LocalName; name = $file.GetAttribute('preferredName'); | |
| 104 | sha256 = $hash; bytes = $bytes.Length } | |
| 105 | } | |
| 106 | $index++ | |
| 107 | } | |
| 108 | [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) | |
| 109 | $all = '' | |
| 110 | $app.GetHierarchy($notebookId, 4, [ref]$all, 1) | |
| 111 | [xml]$finalTree = $all | |
| 112 | $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) | |
| 113 | if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { | |
| 114 | throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' | |
| 115 | } | |
| 116 | [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) | |
| 117 | Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." | |
| 118 | } catch { | |
| 119 | $failure = $_ | |
| 120 | [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 121 | throw | |
| 122 | } finally { | |
| 123 | try { | |
| 124 | try { | |
| 125 | if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } | |
| 126 | if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { | |
| 127 | $app.CloseNotebook($notebookId, $false) | |
| 128 | } | |
| 129 | } catch { | |
| 130 | if ($null -eq $failure) { throw } | |
| 131 | [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | |
| 132 | } | |
| 133 | } finally { | |
| 134 | [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) | |
| 135 | } | |
| 136 | $app = $null | |
| 137 | [GC]::Collect() | |
| 138 | [GC]::WaitForPendingFinalizers() | |
| 139 | if (-not $UseCurrentCache -and -not $CloneHost) { | |
| 140 | Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 | |
| 141 | } | |
| 142 | } |
corpus/page-copy/cold/source.json created+20| ... | ... | @@ -0,0 +1,20 @@ |
| 1 | [ | |
| 2 | { | |
| 3 | "path": "Open Notebook.onetoc2", | |
| 4 | "bytes": 1604, | |
| 5 | "sha256": "bf856a7700054971543fb890257cba327dfd35ef6e5438a41483490c4e06407b", | |
| 6 | "mtime_ns": 1789284797191547350 | |
| 7 | }, | |
| 8 | { | |
| 9 | "path": "copies.one", | |
| 10 | "bytes": 91008, | |
| 11 | "sha256": "8f6c9b66379c4bf6730e281d8363737d88fbe1c521a62fd4d054f735b0505cc7", | |
| 12 | "mtime_ns": 1789284797187906252 | |
| 13 | }, | |
| 14 | { | |
| 15 | "path": "expected-count.txt", | |
| 16 | "bytes": 1, | |
| 17 | "sha256": "19581e27de7ced00ff1ce50b2047e7a567c76b1cbaebabe5ef03f7c3017bb5b7", | |
| 18 | "mtime_ns": 1789284797191633183 | |
| 19 | } | |
| 20 | ] |
corpus/page-copy/cold/teardown.json created+1| ... | ... | @@ -0,0 +1 @@ |
| 1 | {"absent": true} |
crates/notebook/README.md+7| ... | ... | @@ -347,6 +347,13 @@ With the optional `protected` feature, `Notebook::unlock(path, password)` |
| 347 | 347 | reads the pages of a `Locked` section for display; nothing is cached or |
| 348 | 348 | written, and a wrong password is `Error::Protected(PasswordMismatch)`. |
| 349 | 349 | |
| 350 | `Section::import_page(page, author)` copies a page, usually read from another | |
| 351 | section, to the end of this one as a page creation and a save queued like the | |
| 352 | user's own edits, under fresh identities (`Page::copy`); payloads travel with | |
| 353 | the model, so the copy publishes offline later like any save. Content outside | |
| 354 | the model refuses to copy. A move is an import here followed by | |
| 355 | `Section::delete_pages` there. | |
| 356 | ||
| 350 | 357 | `Notebook::refresh` rereads the directory on the caller's schedule and reports |
| 351 | 358 | what another client changed as `Change`s keyed by file identity: a renamed or |
| 352 | 359 | moved section is `Moved`, not removed and added, and a deleted section moves |
crates/notebook/src/session.rs+49-1| ... | ... | @@ -5,7 +5,8 @@ use crate::{ |
| 5 | 5 | ConflictKind, EditStatus, Error, PendingEdit, Remote, Replica, Result, SyncWorker, discover, |
| 6 | 6 | }; |
| 7 | 7 | use onestore::{ |
| 8 | CommitError, ExGuid, PreparedEdit, RevisionIndex, Store, document::Document, page::Page, | |
| 8 | CommitError, ExGuid, PageCreation, PreparedEdit, RevisionIndex, Store, document::Document, | |
| 9 | page::Page, | |
| 9 | 10 | }; |
| 10 | 11 | use std::{ |
| 11 | 12 | collections::BTreeMap, |
| ... | ... | @@ -661,6 +662,53 @@ impl Section { |
| 661 | 662 | Ok(Page::from_space(&Document::parse(&index)?, space)?) |
| 662 | 663 | } |
| 663 | 664 | |
| 665 | /// Copies a page (usually from another section) to the end of this section as a | |
| 666 | /// creation and a save queued like the user's own edits, under fresh identities. | |
| 667 | /// Returns the new page's space. Content outside the model refuses to copy. | |
| 668 | pub fn import_page(&self, page: &Page, author: &str) -> Result<ExGuid> { | |
| 669 | let copy = page.copy()?; | |
| 670 | let creation = PageCreation::new(None, Some(&page.title), author)?; | |
| 671 | self.replica | |
| 672 | .create_page(&self.replica.snapshot()?, &creation)?; | |
| 673 | let space = creation.space(); | |
| 674 | loop { | |
| 675 | // The worker may publish the creation, and so replace the working image, | |
| 676 | // between reading it and saving against it. | |
| 677 | let source = self.replica.snapshot()?; | |
| 678 | let mut after = Page::from_space( | |
| 679 | &Document::parse(&RevisionIndex::parse(&Store::parse(&source)?)?)?, | |
| 680 | space, | |
| 681 | )?; | |
| 682 | after | |
| 683 | .objects | |
| 684 | .retain(|object| matches!(object, onestore::page::PageObject::Title(_))); | |
| 685 | after.objects.extend( | |
| 686 | copy.objects | |
| 687 | .iter() | |
| 688 | .filter(|object| !matches!(object, onestore::page::PageObject::Title(_))) | |
| 689 | .cloned(), | |
| 690 | ); | |
| 691 | after.definitions = copy.definitions.clone(); | |
| 692 | match self.replica.save(&source, space, &after, author) { | |
| 693 | Err(Error::Io(error)) if error.kind() == io::ErrorKind::ResourceBusy => continue, | |
| 694 | result => result?, | |
| 695 | }; | |
| 696 | break; | |
| 697 | } | |
| 698 | self.wake(); | |
| 699 | Ok(space) | |
| 700 | } | |
| 701 | ||
| 702 | /// Removes pages permanently, queued like the user's own edits (a move across | |
| 703 | /// sections is `import_page` there, then this here). | |
| 704 | pub fn delete_pages(&self, pages: &[ExGuid]) -> Result<Option<u64>> { | |
| 705 | let id = self | |
| 706 | .replica | |
| 707 | .delete_pages(&self.replica.snapshot()?, pages)?; | |
| 708 | self.wake(); | |
| 709 | Ok(id) | |
| 710 | } | |
| 711 | ||
| 664 | 712 | /// Saves an edited page. `before` is the model the edit started from; a stored page |
| 665 | 713 | /// that differs from it means the section changed underneath the editor. |
| 666 | 714 | pub fn save(&self, space: ExGuid, before: &Page, after: &Page, author: &str) -> Result<Save> { |
crates/notebook/tests/copy.rs created+242| ... | ... | @@ -0,0 +1,242 @@ |
| 1 | use notebook::{ | |
| 2 | EditStatus, | |
| 3 | session::{Event, Section}, | |
| 4 | }; | |
| 5 | use onestore::{ | |
| 6 | RevisionIndex, Store, | |
| 7 | document::Document, | |
| 8 | page::{Page, PageObject}, | |
| 9 | }; | |
| 10 | use std::{ | |
| 11 | collections::BTreeMap, | |
| 12 | io::Write, | |
| 13 | path::Path, | |
| 14 | time::{Duration, Instant}, | |
| 15 | }; | |
| 16 | ||
| 17 | /// Pages the writer authored for other rows: text with an attachment and its icon, a | |
| 18 | /// table, an inserted picture, a page-level ink drawing, tags, lists, equations | |
| 19 | /// and paragraph formatting. | |
| 20 | const SOURCES: &[&str] = &[ | |
| 21 | "attachment-edit/icon/candidate/files.one", | |
| 22 | "table-edit/created/candidate/tables.one", | |
| 23 | "picture-edit/inserted/candidate/pictures.one", | |
| 24 | "ink-edit/drawing/candidate/ink.one", | |
| 25 | "tag-edit/candidate/tags.one", | |
| 26 | "list-edit/candidate/lists.one", | |
| 27 | "math-edit/written/candidate/math.one", | |
| 28 | "paragraph-format/candidate/synthetic.one", | |
| 29 | ]; | |
| 30 | ||
| 31 | fn pages(bytes: &[u8]) -> Vec<Page> { | |
| 32 | let store = Store::parse(bytes).unwrap(); | |
| 33 | let index = RevisionIndex::parse(&store).unwrap(); | |
| 34 | let document = Document::parse(&index).unwrap(); | |
| 35 | document | |
| 36 | .pages() | |
| 37 | .unwrap() | |
| 38 | .into_iter() | |
| 39 | .map(|(space, _)| Page::from_space(&document, space).unwrap()) | |
| 40 | .collect() | |
| 41 | } | |
| 42 | ||
| 43 | /// Everything but identities and the title object: what a copy must preserve. Identities | |
| 44 | /// are numbered by first appearance in the content, so definitions compare by use. | |
| 45 | fn shape(page: &Page) -> String { | |
| 46 | let mut page = page.clone(); | |
| 47 | flatten(&mut page); | |
| 48 | let mut value = serde_json::to_value(&page).unwrap(); | |
| 49 | let mut names: BTreeMap<String, String> = BTreeMap::new(); | |
| 50 | fn name(names: &mut BTreeMap<String, String>, text: &str) -> String { | |
| 51 | let next = format!("id{}", names.len()); | |
| 52 | names.entry(text.to_owned()).or_insert(next).clone() | |
| 53 | } | |
| 54 | fn scrub(value: &mut serde_json::Value, names: &mut BTreeMap<String, String>) { | |
| 55 | match value { | |
| 56 | serde_json::Value::String(text) if text.parse::<onestore::ExGuid>().is_ok() => { | |
| 57 | *text = name(names, text); | |
| 58 | } | |
| 59 | serde_json::Value::Array(items) => { | |
| 60 | for item in items { | |
| 61 | scrub(item, names); | |
| 62 | } | |
| 63 | } | |
| 64 | serde_json::Value::Object(fields) => { | |
| 65 | // The writer's canonical formatting: an unset flag is false and unset | |
| 66 | // spacing is zero. | |
| 67 | fields.retain(|_, value| { | |
| 68 | !value.is_null() | |
| 69 | && *value != serde_json::Value::Bool(false) | |
| 70 | && value.as_f64() != Some(0.0) | |
| 71 | }); | |
| 72 | let keys: Vec<String> = fields.keys().cloned().collect(); | |
| 73 | for key in keys { | |
| 74 | let mut item = fields.remove(&key).unwrap(); | |
| 75 | scrub(&mut item, names); | |
| 76 | let key = if key.parse::<onestore::ExGuid>().is_ok() { | |
| 77 | name(names, &key) | |
| 78 | } else { | |
| 79 | key | |
| 80 | }; | |
| 81 | fields.insert(key, item); | |
| 82 | } | |
| 83 | } | |
| 84 | _ => {} | |
| 85 | } | |
| 86 | } | |
| 87 | let object = value.as_object_mut().unwrap(); | |
| 88 | object.remove("identity"); | |
| 89 | object.remove("created"); | |
| 90 | object.remove("margin_origin"); | |
| 91 | let objects = object["objects"].as_array_mut().unwrap(); | |
| 92 | objects.retain(|object| object.get("Title").is_none()); | |
| 93 | scrub(&mut object["objects"], &mut names); | |
| 94 | // The destination page brings its own title style. | |
| 95 | object["definitions"] | |
| 96 | .as_object_mut() | |
| 97 | .unwrap() | |
| 98 | .retain(|_, definition| definition["kind"]["type"] != "Style"); | |
| 99 | scrub(&mut object["definitions"], &mut names); | |
| 100 | value.to_string() | |
| 101 | } | |
| 102 | ||
| 103 | /// Moves paragraph-level formatting into each span, where the reader resolves it: the | |
| 104 | /// writer keeps a copied style's formatting on the paragraph or the runs as it sees fit. | |
| 105 | fn flatten(page: &mut Page) { | |
| 106 | fn paragraphs(list: &mut [onestore::page::PageParagraph]) { | |
| 107 | for paragraph in list { | |
| 108 | let format = std::mem::take(&mut paragraph.format); | |
| 109 | match &mut paragraph.content { | |
| 110 | onestore::page::ParagraphContent::Text(text) => { | |
| 111 | let mut at = 0; | |
| 112 | let runs: Vec<(String, onestore::document::Format)> = text | |
| 113 | .text | |
| 114 | .spans() | |
| 115 | .iter() | |
| 116 | .map(|span| { | |
| 117 | let piece = text.text.text()[at..span.end].to_owned(); | |
| 118 | at = span.end; | |
| 119 | (piece, span.format.inherit(&format)) | |
| 120 | }) | |
| 121 | .collect(); | |
| 122 | text.text = onestore::page::Paragraph::from_runs(runs); | |
| 123 | } | |
| 124 | onestore::page::ParagraphContent::Table(table) => { | |
| 125 | for row in &mut table.rows { | |
| 126 | for cell in &mut row.cells { | |
| 127 | paragraphs(&mut cell.paragraphs); | |
| 128 | } | |
| 129 | } | |
| 130 | } | |
| 131 | _ => {} | |
| 132 | } | |
| 133 | } | |
| 134 | } | |
| 135 | for object in &mut page.objects { | |
| 136 | if let PageObject::Outline(outline) = object { | |
| 137 | paragraphs(&mut outline.paragraphs); | |
| 138 | } | |
| 139 | } | |
| 140 | } | |
| 141 | ||
| 142 | /// `NOTEBOOK_COPY_EXPORT` names a new directory receiving the destination section for a | |
| 143 | /// cold reopen. | |
| 144 | #[test] | |
| 145 | fn pages_copy_into_another_section_with_their_content_and_fresh_identities() { | |
| 146 | let temporary = tempfile::tempdir().unwrap(); | |
| 147 | let file = temporary.path().join("copies.one"); | |
| 148 | let cache = temporary.path().join("cache.sqlite"); | |
| 149 | std::fs::write( | |
| 150 | &file, | |
| 151 | onestore::create_section("copies.one", "Destination", "Author").unwrap(), | |
| 152 | ) | |
| 153 | .unwrap(); | |
| 154 | let section = Section::open(&file, &cache, || {}).unwrap(); | |
| 155 | let mut expected = Vec::new(); | |
| 156 | for source in SOURCES { | |
| 157 | let bytes = std::fs::read( | |
| 158 | Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/../../corpus")).join(source), | |
| 159 | ) | |
| 160 | .unwrap(); | |
| 161 | for page in pages(&bytes) { | |
| 162 | if page | |
| 163 | .objects | |
| 164 | .iter() | |
| 165 | .any(|object| matches!(object, PageObject::Unsupported(_))) | |
| 166 | { | |
| 167 | continue; | |
| 168 | } | |
| 169 | let copy = page.copy().unwrap(); | |
| 170 | assert_ne!(copy.identity, page.identity); | |
| 171 | let space = section | |
| 172 | .import_page(&page, "Copier") | |
| 173 | .unwrap_or_else(|e| panic!("{source} {}: {e}", page.title)); | |
| 174 | expected.push((space, page.title.clone(), shape(&copy))); | |
| 175 | } | |
| 176 | } | |
| 177 | assert!(expected.len() >= 8, "{} pages", expected.len()); | |
| 178 | let deadline = Instant::now() + Duration::from_secs(120); | |
| 179 | while section | |
| 180 | .queue() | |
| 181 | .unwrap() | |
| 182 | .iter() | |
| 183 | .any(|edit| !matches!(edit.status, EditStatus::Published { .. })) | |
| 184 | || !section.queue().unwrap().is_empty() | |
| 185 | { | |
| 186 | for event in section.events() { | |
| 187 | if let Event::Failed(message) = event { | |
| 188 | panic!("{message}"); | |
| 189 | } | |
| 190 | } | |
| 191 | assert!( | |
| 192 | Instant::now() < deadline, | |
| 193 | "publication did not finish: {:?}", | |
| 194 | section.queue().unwrap() | |
| 195 | ); | |
| 196 | std::thread::sleep(Duration::from_millis(50)); | |
| 197 | } | |
| 198 | let stored = std::fs::read(&file).unwrap(); | |
| 199 | let written = pages(&stored); | |
| 200 | assert_eq!(written.len(), expected.len() + 1); | |
| 201 | for ((space, title, expected), page) in expected.iter().zip(&written[1..]) { | |
| 202 | let _ = space; | |
| 203 | assert_eq!(&page.title, title); | |
| 204 | let stored = shape(page); | |
| 205 | if &stored != expected { | |
| 206 | let dump = std::env::temp_dir().join("copy-mismatch"); | |
| 207 | std::fs::create_dir_all(&dump).unwrap(); | |
| 208 | std::fs::write(dump.join("expected.json"), expected).unwrap(); | |
| 209 | std::fs::write(dump.join("stored.json"), &stored).unwrap(); | |
| 210 | panic!("{title}: copy differs, see {}", dump.display()); | |
| 211 | } | |
| 212 | } | |
| 213 | section.delete_pages(&[expected.last().unwrap().0]).unwrap(); | |
| 214 | let deadline = Instant::now() + Duration::from_secs(60); | |
| 215 | while !section.queue().unwrap().is_empty() { | |
| 216 | assert!(Instant::now() < deadline); | |
| 217 | std::thread::sleep(Duration::from_millis(50)); | |
| 218 | } | |
| 219 | assert_eq!(pages(&std::fs::read(&file).unwrap()).len(), expected.len()); | |
| 220 | section.close().unwrap(); | |
| 221 | if let Some(directory) = std::env::var_os("NOTEBOOK_COPY_EXPORT") { | |
| 222 | let directory = std::path::PathBuf::from(directory); | |
| 223 | std::fs::create_dir_all(&directory).unwrap(); | |
| 224 | let bytes = std::fs::read(&file).unwrap(); | |
| 225 | std::fs::File::create_new(directory.join("copies.one")) | |
| 226 | .unwrap() | |
| 227 | .write_all(&bytes) | |
| 228 | .unwrap(); | |
| 229 | let file_id = Store::parse(&bytes).unwrap().header.file_id; | |
| 230 | std::fs::write( | |
| 231 | directory.join("Open Notebook.onetoc2"), | |
| 232 | onestore::create_table_of_contents("Open Notebook.onetoc2", &[("copies.one", file_id)]) | |
| 233 | .unwrap(), | |
| 234 | ) | |
| 235 | .unwrap(); | |
| 236 | std::fs::write( | |
| 237 | directory.join("expected-count.txt"), | |
| 238 | expected.len().to_string(), | |
| 239 | ) | |
| 240 | .unwrap(); | |
| 241 | } | |
| 242 | } |
crates/onestore/Cargo.toml+3-3| ... | ... | @@ -5,14 +5,15 @@ edition = "2024" |
| 5 | 5 | publish = false |
| 6 | 6 | |
| 7 | 7 | [features] |
| 8 | protected = ["dep:aes", "dep:base64", "dep:cbc", "dep:roxmltree", "dep:sha1", "dep:subtle", "dep:zeroize"] | |
| 8 | protected = ["dep:aes", "dep:cbc", "dep:roxmltree", "dep:sha1", "dep:subtle", "dep:zeroize"] | |
| 9 | 9 | |
| 10 | 10 | [dependencies] |
| 11 | base64 = "0.22.1" | |
| 12 | serde_json = "1.0.151" | |
| 11 | 13 | md5 = "0.8.1" |
| 12 | 14 | getrandom = "0.4.3" |
| 13 | 15 | serde = { version = "1.0.229", features = ["derive"] } |
| 14 | 16 | aes = { version = "0.9.3", features = ["zeroize"], optional = true } |
| 15 | base64 = { version = "0.22.1", optional = true } | |
| 16 | 17 | cbc = { version = "0.2.1", features = ["zeroize"], optional = true } |
| 17 | 18 | roxmltree = { version = "0.21.1", optional = true } |
| 18 | 19 | sha1 = { version = "0.11.0", features = ["zeroize"], optional = true } |
| ... | ... | @@ -23,4 +24,3 @@ zeroize = { version = "1.9.0", optional = true } |
| 23 | 24 | nix = { version = "0.31.3", default-features = false, features = ["fs"] } |
| 24 | 25 | |
| 25 | 26 | [dev-dependencies] |
| 26 | serde_json = "1.0.151" |
crates/onestore/README.md+2-1| ... | ... | @@ -43,6 +43,7 @@ harness also accepts `--client-profile release`. |
| 43 | 43 | | `Object::file_reference`, `Store::file_data` | Identify internal/external payloads and read internal payload bytes | |
| 44 | 44 | | `document::Document`, `Revision::text_runs` | Interpret document objects and inherited text formatting while retaining unknown properties and revision identities | |
| 45 | 45 | | `Document::active`, `Document::pages_in`, `Revision::parents`, `RevisionIndex::active` | Resolve the active revision, the pages of a space and parent links without repeating the lookups | |
| 46 | | `Page::copy` | The page's content under fresh identities for copying into another section, definitions and payloads included | | |
| 46 | 47 | | `page::link::internal_link`, `page::link::parse_internal_link` | Build and read the `onenote:#…` URLs OneNote stores for links to sections, pages and paragraphs, by identity | |
| 47 | 48 | | `page::Page`, `page::Paragraph`, `page::Ink`, `page::Math` | Build an editable page model (title, outlines, paragraphs with coalesced text spans, tables, images, attachments, ink drawings and handwriting decoded to stroke polylines in page points) with stored identities; equations parse from their linear text and run data into a tree that renders the MathML OneNote exports; content outside the model is retained as `Unsupported` | |
| 48 | 49 | | `protected::UnlockedSection` (optional feature) | Own decoded buffers for explicit known-password inspection; clear those buffers on drop; derived document strings/exports remain caller-owned | |
| ... | ... | @@ -60,7 +61,7 @@ harness also accepts `--client-profile release`. |
| 60 | 61 | | `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules | |
| 61 | 62 | | `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | |
| 62 | 63 | | `OutlineEdit`, `PreparedEdit::outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content | |
| 63 | | `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks (external and, via `page::link`, to pages, paragraphs and sections), bullets and numbering, note tags, table rows, columns, cell shading and indents, nested tables, inserted pictures (in paragraphs or on the page) and attachments, picture position, size and description, a stored attachment's shown name, source path and icon size, ink drawings and handwriting (strokes added and erased), equations (built from `page::Math` trees), paragraph insertion/split/join/move/deletion, outline insertion/deletion/position/width and saved collapse state, lowered onto the typed writers with the model's identities | | |
| 64 | | `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks (external and, via `page::link`, to pages, paragraphs and sections), bullets and numbering, note tags, table rows, columns, cell shading and indents, nested tables, inserted pictures (in paragraphs or on the page) and attachments, picture position, size and description, a stored attachment's shown name, source path and icon size, paragraph styles on new paragraphs, ink drawings and handwriting (strokes added and erased), equations (built from `page::Math` trees), paragraph insertion/split/join/move/deletion, outline insertion/deletion/position/width and saved collapse state, lowered onto the typed writers with the model's identities | | |
| 64 | 65 | | `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically | |
| 65 | 66 | | `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter | |
| 66 | 67 | | `read_file` | Read a snapshot under whole-file exclusion | |
crates/onestore/src/page/mod.rs+96-3| ... | ... | @@ -18,6 +18,38 @@ pub mod text; |
| 18 | 18 | pub use ink::{Ink, InkStroke}; |
| 19 | 19 | pub use math::Math; |
| 20 | 20 | pub(crate) mod write; |
| 21 | ||
| 22 | /// Picture and attachment payloads travel with the model (queued intents replay them), | |
| 23 | /// as base64 text. | |
| 24 | mod payload { | |
| 25 | use base64::Engine; | |
| 26 | use std::sync::Arc; | |
| 27 | ||
| 28 | pub fn serialize<S: serde::Serializer>( | |
| 29 | bytes: &Option<Arc<[u8]>>, | |
| 30 | serializer: S, | |
| 31 | ) -> Result<S::Ok, S::Error> { | |
| 32 | match bytes { | |
| 33 | Some(bytes) => { | |
| 34 | serializer.serialize_some(&base64::engine::general_purpose::STANDARD.encode(bytes)) | |
| 35 | } | |
| 36 | None => serializer.serialize_none(), | |
| 37 | } | |
| 38 | } | |
| 39 | ||
| 40 | pub fn deserialize<'de, D: serde::Deserializer<'de>>( | |
| 41 | deserializer: D, | |
| 42 | ) -> Result<Option<Arc<[u8]>>, D::Error> { | |
| 43 | let text: Option<String> = serde::Deserialize::deserialize(deserializer)?; | |
| 44 | text.map(|text| { | |
| 45 | base64::engine::general_purpose::STANDARD | |
| 46 | .decode(text) | |
| 47 | .map(Arc::from) | |
| 48 | .map_err(serde::de::Error::custom) | |
| 49 | }) | |
| 50 | .transpose() | |
| 51 | } | |
| 52 | } | |
| 21 | 53 | pub use text::Paragraph; |
| 22 | 54 | |
| 23 | 55 | /// The role of a title-outline paragraph that displays the page's creation date or time. |
| ... | ... | @@ -140,9 +172,9 @@ pub struct Attachment { |
| 140 | 172 | pub source_path: Option<String>, |
| 141 | 173 | /// The icon OneNote rendered for the file, when present. |
| 142 | 174 | pub size: Option<[f32; 2]>, |
| 143 | #[serde(skip)] | |
| 175 | #[serde(with = "payload")] | |
| 144 | 176 | pub bytes: Option<Arc<[u8]>>, |
| 145 | #[serde(skip)] | |
| 177 | #[serde(with = "payload")] | |
| 146 | 178 | pub preview: Option<Arc<[u8]>>, |
| 147 | 179 | } |
| 148 | 180 | |
| ... | ... | @@ -245,7 +277,7 @@ pub struct Image { |
| 245 | 277 | pub layout: Layout, |
| 246 | 278 | /// Displayed picture width and height in points. |
| 247 | 279 | pub size: Option<[f32; 2]>, |
| 248 | #[serde(skip)] | |
| 280 | #[serde(with = "payload")] | |
| 249 | 281 | pub bytes: Option<Arc<[u8]>>, |
| 250 | 282 | pub alt: Option<String>, |
| 251 | 283 | pub background: bool, |
| ... | ... | @@ -331,6 +363,67 @@ impl Page { |
| 331 | 363 | Self::from_revision(revision, id) |
| 332 | 364 | } |
| 333 | 365 | |
| 366 | /// The page's content under fresh identities, for a copy into this or another section: | |
| 367 | /// OneNote gives a copied page and every object on it new identities, and stored | |
| 368 | /// links keep naming the original. Content outside the model has no copy, so an | |
| 369 | /// `Unsupported` object is an error. | |
| 370 | pub fn copy(&self) -> Result<Self, Error> { | |
| 371 | let invalid = |message| Error { offset: 0, message }; | |
| 372 | let mut value = | |
| 373 | serde_json::to_value(self).map_err(|_| invalid("Page is not serializable"))?; | |
| 374 | let mut fresh: BTreeMap<String, String> = BTreeMap::new(); | |
| 375 | fn walk( | |
| 376 | value: &mut serde_json::Value, | |
| 377 | fresh: &mut BTreeMap<String, String>, | |
| 378 | ) -> Result<(), Error> { | |
| 379 | match value { | |
| 380 | serde_json::Value::String(text) if text.starts_with('{') => { | |
| 381 | if text.parse::<ExGuid>().is_ok() { | |
| 382 | if !fresh.contains_key(text) { | |
| 383 | fresh.insert(text.clone(), text::new_id()?.to_string()); | |
| 384 | } | |
| 385 | *text = fresh[text].clone(); | |
| 386 | } | |
| 387 | } | |
| 388 | serde_json::Value::Array(items) => { | |
| 389 | for item in items { | |
| 390 | walk(item, fresh)?; | |
| 391 | } | |
| 392 | } | |
| 393 | serde_json::Value::Object(fields) => { | |
| 394 | if fields.contains_key("Unsupported") { | |
| 395 | return Err(Error { | |
| 396 | offset: 0, | |
| 397 | message: "Content outside the page model cannot be copied", | |
| 398 | }); | |
| 399 | } | |
| 400 | let keys: Vec<String> = fields.keys().cloned().collect(); | |
| 401 | for key in keys { | |
| 402 | let mut item = fields.remove(&key).unwrap(); | |
| 403 | walk(&mut item, fresh)?; | |
| 404 | let key = match key.parse::<ExGuid>() { | |
| 405 | Ok(_) => { | |
| 406 | if !fresh.contains_key(&key) { | |
| 407 | fresh.insert(key.clone(), text::new_id()?.to_string()); | |
| 408 | } | |
| 409 | fresh[&key].clone() | |
| 410 | } | |
| 411 | Err(_) => key, | |
| 412 | }; | |
| 413 | fields.insert(key, item); | |
| 414 | } | |
| 415 | } | |
| 416 | _ => {} | |
| 417 | } | |
| 418 | Ok(()) | |
| 419 | } | |
| 420 | walk(&mut value, &mut fresh)?; | |
| 421 | let mut copy: Self = | |
| 422 | serde_json::from_value(value).map_err(|_| invalid("Page copy does not deserialize"))?; | |
| 423 | copy.identity = Some(crate::write::fresh_guid()?); | |
| 424 | Ok(copy) | |
| 425 | } | |
| 426 | ||
| 334 | 427 | /// The notebook-management identity of the page in an active page revision, which |
| 335 | 428 | /// internal links name. |
| 336 | 429 | pub fn identity_of(revision: &Revision<'_>) -> Option<[u8; 16]> { |
crates/onestore/src/page/write.rs+136-8| ... | ... | @@ -496,7 +496,10 @@ impl Lowering<'_> { |
| 496 | 496 | return Err(invalid("Page creation time and margins cannot be edited")); |
| 497 | 497 | } |
| 498 | 498 | for (id, definition) in &after.definitions { |
| 499 | if before.definitions.get(id) != Some(definition) | |
| 499 | if before | |
| 500 | .definitions | |
| 501 | .get(id) | |
| 502 | .is_some_and(|stored| stored != definition) | |
| 500 | 503 | && !matches!( |
| 501 | 504 | definition.kind, |
| 502 | 505 | Kind::List { .. } | Kind::TagDefinition { .. } |
| ... | ... | @@ -540,6 +543,7 @@ impl Lowering<'_> { |
| 540 | 543 | self.delete(&old, &new, &consumed)?; |
| 541 | 544 | self.edit_equations(&new)?; |
| 542 | 545 | self.edit_text(&new)?; |
| 546 | self.edit_paragraph_styles(&old, &new, after)?; | |
| 543 | 547 | self.edit_lists(after, &new)?; |
| 544 | 548 | self.edit_tags(after, &new)?; |
| 545 | 549 | self.edit_paragraph_formatting(&new)?; |
| ... | ... | @@ -685,10 +689,23 @@ impl Lowering<'_> { |
| 685 | 689 | let Some(first) = new.children[id].first().copied() else { |
| 686 | 690 | return Err(invalid("A new outline needs a paragraph")); |
| 687 | 691 | }; |
| 688 | let text = new | |
| 689 | .text(first) | |
| 690 | .ok_or_else(|| invalid("A new outline starts with a text paragraph"))?; | |
| 691 | let insertion = Insertion::outline(self.page, x, y, text.text.text(), self.author)?; | |
| 692 | // A new outline starts as one text paragraph; other content replaces it in | |
| 693 | // its own pass, as for a paragraph inserted into an existing outline. | |
| 694 | let (text, text_id) = match &new.paragraphs[&first].content { | |
| 695 | ParagraphContent::Text(text) if text.date_field.is_none() => { | |
| 696 | (text.text.text(), Some(text.id)) | |
| 697 | } | |
| 698 | ParagraphContent::Table(_) | |
| 699 | | ParagraphContent::Image(_) | |
| 700 | | ParagraphContent::Attachment(_) | |
| 701 | | ParagraphContent::Ink(_) => ("", None), | |
| 702 | _ => { | |
| 703 | return Err(invalid( | |
| 704 | "A new outline starts with a paragraph the writer builds", | |
| 705 | )); | |
| 706 | } | |
| 707 | }; | |
| 708 | let insertion = Insertion::outline(self.page, x, y, text, self.author)?; | |
| 692 | 709 | let space = self.space; |
| 693 | 710 | self.apply(|image| insertion.apply(image, space))?; |
| 694 | 711 | let outline_id = insertion.object(); |
| ... | ... | @@ -700,7 +717,9 @@ impl Lowering<'_> { |
| 700 | 717 | n: 3, |
| 701 | 718 | }, |
| 702 | 719 | ); |
| 703 | self.alias.insert(text.id, insertion.text_object()); | |
| 720 | if let Some(text_id) = text_id { | |
| 721 | self.alias.insert(text_id, insertion.text_object()); | |
| 722 | } | |
| 704 | 723 | placed.insert(*id, vec![first]); |
| 705 | 724 | placed.insert(first, Vec::new()); |
| 706 | 725 | page_order.push(*id); |
| ... | ... | @@ -1428,9 +1447,16 @@ impl Lowering<'_> { |
| 1428 | 1447 | let anchor = next.map(|n| self.id(n)); |
| 1429 | 1448 | if !old.paragraphs.contains_key(id) && !self.alias.contains_key(id) { |
| 1430 | 1449 | let paragraph = new.paragraphs[id]; |
| 1431 | if paragraph.style.is_some() { | |
| 1450 | // A styled paragraph appended to a stored outline is a split whose | |
| 1451 | // precondition failed under a merge; it stays for review. A new outline | |
| 1452 | // takes styled paragraphs as built (a copied page). | |
| 1453 | let mut root = *container; | |
| 1454 | while let Some(parent) = new.container.get(&root) { | |
| 1455 | root = *parent; | |
| 1456 | } | |
| 1457 | if paragraph.style.is_some() && old.outlines.contains_key(&root) { | |
| 1432 | 1458 | return Err(invalid( |
| 1433 | "New paragraphs contain plain text without fields or styles", | |
| 1459 | "New paragraphs in a stored outline contain plain text without styles", | |
| 1434 | 1460 | )); |
| 1435 | 1461 | } |
| 1436 | 1462 | // A new table starts as an empty text paragraph whose content the |
| ... | ... | @@ -2285,6 +2311,93 @@ impl Lowering<'_> { |
| 2285 | 2311 | Ok(()) |
| 2286 | 2312 | } |
| 2287 | 2313 | |
| 2314 | /// A new paragraph, or one whose style changed, references its paragraph style from | |
| 2315 | /// its text object; a style the page has not stored yet is created from its | |
| 2316 | /// definition, as OneNote keeps quick styles. | |
| 2317 | fn edit_paragraph_styles( | |
| 2318 | &mut self, | |
| 2319 | old: &View<'_>, | |
| 2320 | new: &View<'_>, | |
| 2321 | after: &Page, | |
| 2322 | ) -> Result<(), Error> { | |
| 2323 | let current = self.current()?; | |
| 2324 | for (id, paragraph) in &new.paragraphs { | |
| 2325 | let Some(definition) = paragraph.style else { | |
| 2326 | continue; | |
| 2327 | }; | |
| 2328 | if old | |
| 2329 | .paragraphs | |
| 2330 | .get(id) | |
| 2331 | .is_some_and(|previous| previous.style == paragraph.style) | |
| 2332 | { | |
| 2333 | continue; | |
| 2334 | } | |
| 2335 | let Some(text) = paragraph.text() else { | |
| 2336 | return Err(invalid("Only text paragraphs take a paragraph style")); | |
| 2337 | }; | |
| 2338 | let known = current.definitions.contains_key(&definition) | |
| 2339 | || self.alias.contains_key(&definition); | |
| 2340 | let style_id = if known { | |
| 2341 | self.id(definition) | |
| 2342 | } else { | |
| 2343 | let allocated = ExGuid { | |
| 2344 | guid: crate::write::fresh_guid()?, | |
| 2345 | n: 1, | |
| 2346 | }; | |
| 2347 | self.alias.insert(definition, allocated); | |
| 2348 | allocated | |
| 2349 | }; | |
| 2350 | let values = if known { | |
| 2351 | None | |
| 2352 | } else { | |
| 2353 | let Some(model) = after.definitions.get(&definition) else { | |
| 2354 | return Err(invalid("A paragraph references a missing style definition")); | |
| 2355 | }; | |
| 2356 | let Kind::Style { name } = &model.kind else { | |
| 2357 | return Err(invalid("A paragraph style must be a style definition")); | |
| 2358 | }; | |
| 2359 | let mut values = style_values(&model.format); | |
| 2360 | if let Some(name) = name { | |
| 2361 | values.push((0x1c00345a, crate::create::string(name))); | |
| 2362 | } | |
| 2363 | if let Some(alignment) = model.format.alignment { | |
| 2364 | values.push((0x0c003477, vec![alignment])); | |
| 2365 | } | |
| 2366 | for (property, value) in [ | |
| 2367 | (0x1400342e, model.format.space_before), | |
| 2368 | (0x1400342f, model.format.space_after), | |
| 2369 | (0x14003430, model.format.line_spacing), | |
| 2370 | ] { | |
| 2371 | if let Some(points) = value { | |
| 2372 | values.push((property, (points / 36.0).to_le_bytes().to_vec())); | |
| 2373 | } | |
| 2374 | } | |
| 2375 | Some(values) | |
| 2376 | }; | |
| 2377 | let (space, object) = (self.space, self.id(text.id)); | |
| 2378 | self.apply(|image| { | |
| 2379 | crate::write::write_revision(image, space, |raw| { | |
| 2380 | let mut changed = BTreeMap::new(); | |
| 2381 | if let Some(values) = &values { | |
| 2382 | let mut node = PropertyObject { | |
| 2383 | jcid: 0x12004d, | |
| 2384 | bytes: crate::create::properties(values)?, | |
| 2385 | global_ids: std::sync::Arc::new(BTreeMap::from([(0, style_id.guid)])), | |
| 2386 | }; | |
| 2387 | node.reference(style_id)?; | |
| 2388 | changed.insert(style_id, node); | |
| 2389 | } | |
| 2390 | let mut target = PropertyObject::from_object(&raw.objects[&object])?; | |
| 2391 | let reference = target.reference(style_id)?; | |
| 2392 | target.set(&[(0x2000342c, &reference)])?; | |
| 2393 | changed.insert(object, target); | |
| 2394 | Ok(changed) | |
| 2395 | }) | |
| 2396 | })?; | |
| 2397 | } | |
| 2398 | Ok(()) | |
| 2399 | } | |
| 2400 | ||
| 2288 | 2401 | fn edit_paragraph_formatting(&mut self, new: &View<'_>) -> Result<(), Error> { |
| 2289 | 2402 | let current = self.current()?; |
| 2290 | 2403 | let current = View::new(&current)?; |
| ... | ... | @@ -2492,6 +2605,21 @@ impl Lowering<'_> { |
| 2492 | 2605 | .get(&self.id(*id)) |
| 2493 | 2606 | .ok_or_else(|| invalid("An outline is missing after text edits"))?; |
| 2494 | 2607 | let (space, object) = (self.space, self.id(*id)); |
| 2608 | // A new outline takes the model's indentation table (a copied outline keeps | |
| 2609 | // its levels' offsets); a stored table stays as it is. | |
| 2610 | if !old.outlines.contains_key(id) | |
| 2611 | && !outline.indents.is_empty() | |
| 2612 | && outline.indents != stored.indents | |
| 2613 | { | |
| 2614 | let indents = measurement_bytes(&outline.indents, 4)?; | |
| 2615 | self.apply(|current| { | |
| 2616 | crate::write::write_revision(current, space, |raw| { | |
| 2617 | let mut node = PropertyObject::from_object(&raw.objects[&object])?; | |
| 2618 | node.set(&[(0x1c001c12, &indents)])?; | |
| 2619 | Ok(BTreeMap::from([(object, node)])) | |
| 2620 | }) | |
| 2621 | })?; | |
| 2622 | } | |
| 2495 | 2623 | if (outline.layout.x, outline.layout.y) != (stored.layout.x, stored.layout.y) { |
| 2496 | 2624 | let (Some(x), Some(y)) = (outline.layout.x, outline.layout.y) else { |
| 2497 | 2625 | return Err(invalid("An outline position needs both coordinates")); |
tools/test_page_copy.py created+34| ... | ... | @@ -0,0 +1,34 @@ |
| 1 | from pathlib import Path | |
| 2 | import runpy | |
| 3 | import shutil | |
| 4 | from tempfile import TemporaryDirectory | |
| 5 | import unittest | |
| 6 | import xml.etree.ElementTree as ET | |
| 7 | ||
| 8 | from native_xml import ns | |
| 9 | ||
| 10 | ROOT = Path(__file__).resolve().parent.parent | |
| 11 | FIXTURE = ROOT / 'corpus/page-copy' | |
| 12 | compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] | |
| 13 | ||
| 14 | ||
| 15 | class PageCopyTest(unittest.TestCase): | |
| 16 | def test_pages_copied_between_sections_read_natively(self): | |
| 17 | with TemporaryDirectory() as temporary: | |
| 18 | read = Path(temporary) / 'read' | |
| 19 | shutil.copytree(FIXTURE / 'cold/read', read) | |
| 20 | compare(FIXTURE / 'candidate', read) | |
| 21 | pages = [ET.parse(path).getroot() for path in sorted((FIXTURE / 'cold/read').glob('page-*.xml'))] | |
| 22 | self.assertEqual(len(pages), int((FIXTURE / 'candidate/expected-count.txt').read_text())) | |
| 23 | kinds = {tag for page in pages for tag in {node.tag.rsplit('}', 1)[-1] for node in page.iter()}} | |
| 24 | self.assertTrue({'InsertedFile', 'Image', 'Table', 'InkDrawing', 'Tag', 'List', 'Bullet', 'Number'} <= kinds, kinds) | |
| 25 | hierarchy = ET.parse(FIXTURE / 'cold/read/hierarchy.xml').getroot() | |
| 26 | titles = [page.get('name') for page in hierarchy.iter('{%s}Page' % ns['one'])] | |
| 27 | self.assertEqual(titles[0], 'Destination') | |
| 28 | self.assertNotIn('Paragraph formatting', titles) | |
| 29 | self.assertEqual(len(set(titles)), len(titles)) | |
| 30 | self.assertEqual(len(set(titles)), len(titles)) | |
| 31 | ||
| 32 | ||
| 33 | if __name__ == '__main__': | |
| 34 | unittest.main() |