From 59c8648048632812f85fd595c05aa9720ad2a051 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Sat, 12 Sep 2026 12:16:00 -0700 Subject: [PATCH] feat: write table rows, columns and new tables through the page model Table structure in the page model is a difference the writer now lowers. Native rows and cells are plain containers: a new cell carries the modification time, the indent array copied from a sibling cell and the flags every native cell has; a new row lists its cells; every table has its row and cell order rebuilt to the model, and its column count, widths (in inches), lock bitmap and border flag rewritten. A new table starts as an empty text paragraph placed like any other, whose content the structure pass replaces with the table before the deferred cell containers receive their paragraphs. Paragraphs of removed rows and cells are covered by the removal. Table identity, layout and tags, and cell layout, indents and shading stay fixed; an emptied cell keeps the replacement paragraph the tree writer provides. corpus/table-edit is the native gate: a Rust-created two-by-two table on a fresh page, and a native table on the tree fixture with a third column and a second row added, both cold-read by OneNote 2010 with widths, locks, borders, rows and cell text intact; tools/test_table_edit.py checks both without a VM. Assisted-by: claude-fable-5.1 --- corpus/table-edit/README.md | 28 ++ .../created/candidate/Open Notebook.onetoc2 | Bin 0 -> 1604 bytes .../table-edit/created/candidate/tables.one | Bin 0 -> 11016 bytes corpus/table-edit/created/cold/commands.jsonl | 3 + corpus/table-edit/created/cold/machine.json | 1 + .../cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 3764 bytes .../created/cold/notebook/tables.one | Bin 0 -> 12512 bytes .../created/cold/read/environment.json | 7 + .../created/cold/read/hierarchy.xml | 2 + .../table-edit/created/cold/read/page-000.xml | 2 + .../created/cold/read/payloads.json | 3 + corpus/table-edit/created/cold/run.json | 18 + .../table-edit/created/cold/scripts/cold.ps1 | 27 ++ .../table-edit/created/cold/scripts/read.ps1 | 142 +++++++ corpus/table-edit/created/cold/source.json | 14 + corpus/table-edit/created/cold/teardown.json | 1 + .../edited/candidate/Open Notebook.onetoc2 | Bin 0 -> 1612 bytes .../table-edit/edited/candidate/synthetic.one | Bin 0 -> 92288 bytes corpus/table-edit/edited/cold/commands.jsonl | 3 + corpus/table-edit/edited/cold/machine.json | 1 + .../cold/notebook/Open Notebook.onetoc2 | Bin 0 -> 3780 bytes .../edited/cold/notebook/synthetic.one | Bin 0 -> 96016 bytes .../edited/cold/read/environment.json | 7 + .../table-edit/edited/cold/read/hierarchy.xml | 2 + .../table-edit/edited/cold/read/page-000.xml | 5 + .../table-edit/edited/cold/read/page-001.xml | 5 + .../table-edit/edited/cold/read/page-002.xml | 5 + .../table-edit/edited/cold/read/page-003.xml | 5 + .../table-edit/edited/cold/read/page-004.xml | 5 + .../table-edit/edited/cold/read/page-005.xml | 20 + .../table-edit/edited/cold/read/page-006.xml | 4 + .../table-edit/edited/cold/read/page-007.xml | 5 + .../table-edit/edited/cold/read/page-008.xml | 4 + .../table-edit/edited/cold/read/page-009.xml | 5 + .../table-edit/edited/cold/read/page-010.xml | 5 + .../table-edit/edited/cold/read/page-011.xml | 5 + .../table-edit/edited/cold/read/payloads.json | 3 + corpus/table-edit/edited/cold/run.json | 18 + .../table-edit/edited/cold/scripts/cold.ps1 | 27 ++ .../table-edit/edited/cold/scripts/read.ps1 | 142 +++++++ corpus/table-edit/edited/cold/source.json | 14 + corpus/table-edit/edited/cold/teardown.json | 1 + crates/notebook/README.md | 2 +- crates/onestore/README.md | 2 +- crates/onestore/src/page/write.rs | 356 +++++++++++++++--- crates/onestore/tests/page_tables.rs | 306 +++++++++++++++ crates/onestore/tests/page_writer.rs | 2 +- tools/test_table_edit.py | 58 +++ 48 files changed, 1219 insertions(+), 46 deletions(-) create mode 100644 corpus/table-edit/README.md create mode 100644 corpus/table-edit/created/candidate/Open Notebook.onetoc2 create mode 100644 corpus/table-edit/created/candidate/tables.one create mode 100644 corpus/table-edit/created/cold/commands.jsonl create mode 100644 corpus/table-edit/created/cold/machine.json create mode 100644 corpus/table-edit/created/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/table-edit/created/cold/notebook/tables.one create mode 100644 corpus/table-edit/created/cold/read/environment.json create mode 100644 corpus/table-edit/created/cold/read/hierarchy.xml create mode 100644 corpus/table-edit/created/cold/read/page-000.xml create mode 100644 corpus/table-edit/created/cold/read/payloads.json create mode 100644 corpus/table-edit/created/cold/run.json create mode 100644 corpus/table-edit/created/cold/scripts/cold.ps1 create mode 100644 corpus/table-edit/created/cold/scripts/read.ps1 create mode 100644 corpus/table-edit/created/cold/source.json create mode 100644 corpus/table-edit/created/cold/teardown.json create mode 100644 corpus/table-edit/edited/candidate/Open Notebook.onetoc2 create mode 100644 corpus/table-edit/edited/candidate/synthetic.one create mode 100644 corpus/table-edit/edited/cold/commands.jsonl create mode 100644 corpus/table-edit/edited/cold/machine.json create mode 100644 corpus/table-edit/edited/cold/notebook/Open Notebook.onetoc2 create mode 100644 corpus/table-edit/edited/cold/notebook/synthetic.one create mode 100644 corpus/table-edit/edited/cold/read/environment.json create mode 100644 corpus/table-edit/edited/cold/read/hierarchy.xml create mode 100644 corpus/table-edit/edited/cold/read/page-000.xml create mode 100644 corpus/table-edit/edited/cold/read/page-001.xml create mode 100644 corpus/table-edit/edited/cold/read/page-002.xml create mode 100644 corpus/table-edit/edited/cold/read/page-003.xml create mode 100644 corpus/table-edit/edited/cold/read/page-004.xml create mode 100644 corpus/table-edit/edited/cold/read/page-005.xml create mode 100644 corpus/table-edit/edited/cold/read/page-006.xml create mode 100644 corpus/table-edit/edited/cold/read/page-007.xml create mode 100644 corpus/table-edit/edited/cold/read/page-008.xml create mode 100644 corpus/table-edit/edited/cold/read/page-009.xml create mode 100644 corpus/table-edit/edited/cold/read/page-010.xml create mode 100644 corpus/table-edit/edited/cold/read/page-011.xml create mode 100644 corpus/table-edit/edited/cold/read/payloads.json create mode 100644 corpus/table-edit/edited/cold/run.json create mode 100644 corpus/table-edit/edited/cold/scripts/cold.ps1 create mode 100644 corpus/table-edit/edited/cold/scripts/read.ps1 create mode 100644 corpus/table-edit/edited/cold/source.json create mode 100644 corpus/table-edit/edited/cold/teardown.json create mode 100644 crates/onestore/tests/page_tables.rs create mode 100644 tools/test_table_edit.py diff --git a/corpus/table-edit/README.md b/corpus/table-edit/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d96fe5fce55b69f51424cb25b55ee36881fb0316 --- /dev/null +++ b/corpus/table-edit/README.md @@ -0,0 +1,28 @@ +# Rust table authoring + +`created/candidate` is the page writer's output for +`a_new_table_is_created_on_a_fresh_page` in +`crates/onestore/tests/page_tables.rs`: on a section created in Rust, a +paragraph between two text paragraphs becomes a two-by-two table with locked +144 pt and 216 pt columns, visible borders and a text paragraph in every +cell. `created/cold` is a fresh OneNote 2010 read showing both columns with +their widths and locks, both rows and every cell text. + +`edited/candidate` is the writer's output for +`a_row_and_a_column_are_added_to_a_native_table_and_removed_again` on +`corpus/outline-edit/tree/before`: the one-row, two-column native table on +"Delete cell subtree" gains an unlocked 120 pt third column and a second row +of three cells. `edited/cold` is its cold read: the native columns keep their +widths and locks, OneNote sizes the unlocked column to its content, and the +new row and cells read back with their text. + +Native rows and cells are plain containers; the writer creates them with the +modification time, the indent array copied from a sibling cell, and the flags +every native cell carries, rebuilds each table's row and cell order to the +model's, and writes the column count, widths, locks and border flag. New +tables start as an empty paragraph whose content the structure pass replaces. +An emptied cell keeps a replacement paragraph, as the tree writer provides. +`tools/test_table_edit.py` checks both captures without a VM. Regenerate with +`ONESTORE_TABLE_EXPORT` and `ONESTORE_TABLE_EDIT_EXPORT` set to new absolute +directories while running the tests, then cold-open them with +`tools/native_runner.py OUTPUT COLD --expected-pages 1|12 --collect-notebook`. diff --git a/corpus/table-edit/created/candidate/Open Notebook.onetoc2 b/corpus/table-edit/created/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..784f65349498d29d5f5fe5433dd751c740326029 GIT binary patch literal 1604 zcmZ3e|KIuM`!b(-j|5(YE0x913XTxws*d%Y{ZYYwQf21?1~g!Qcb0(k#IK%9FNjZk zdt%)UX`nnZhAICK1RypeOaMy5#L! zFMq!5(vsD_3#MK=tF6QSRYQQGL9c?LgMkI4i-92kh#_78@jyC2vH?(836LyU93%t6 zkp@67L)kJwv%p~i3lHyI#!ZJs4|T)*F9cKw0z5#+pgRy`n1BPrgbb)vVAFwOAhSRK zqz~OZVUQpYa7+N|gO-mV4`4H!1&5sw(^1U_#S_R=Aonr>F~}Ye4RYhMZS_--^#A}X CpVu+~ literal 0 HcmV?d00001 diff --git a/corpus/table-edit/created/candidate/tables.one b/corpus/table-edit/created/candidate/tables.one new file mode 100644 index 0000000000000000000000000000000000000000..75d44cc0da8f1738e0438647e48a8b09cb844710 GIT binary patch literal 11016 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 Zf@kBB2zorEhZ=aOfd|xpaSVXye*wvKn~MMd literal 0 HcmV?d00001 diff --git a/corpus/table-edit/created/cold/commands.jsonl b/corpus/table-edit/created/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..613db96ba12c5a91daa79bc91aa59630b63ba5bc --- /dev/null +++ b/corpus/table-edit/created/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-838F23A9 -ExpectedPages 1", "exit": 0, "stdout": "Read 1 sections and 1 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/table-edit/created/cold/machine.json b/corpus/table-edit/created/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..0a5fbe5a162b6b7a317dc04cb9656215268b42fa --- /dev/null +++ b/corpus/table-edit/created/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-838f23a9", "hostname": "ONE-M6-838F23A9"} diff --git a/corpus/table-edit/created/cold/notebook/Open Notebook.onetoc2 b/corpus/table-edit/created/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..72c04c527a22a5243a61e843e00244604e967e55 GIT binary patch literal 3764 zcmZ3e|KIuM`!b(-j|5(YE0x913XTxws*d%Y{ZYYwQf21?1~g!Qcb0(k#IK%9FNjZk zdt%)UX`nnZhAICK1RypOOaMy5#L#dC{V|eoI6)?_j-LH;Wl6#Su9DlA+Edm;J|C)=r6g%7xH@owY@CAwLXb zS1~F&8UjN;1R7WvrpPkL?37^;$p+H3Y3 zFeEZ0G2}3$G88lDG2}Dk0a=jz2&5Q*sv5X}1S7*Y83q-Q9tb${<@?u-Rbfj6p?r}7 zhK6?+SWa(#c+_)Gy+VBc!bj~e)zU8*8s^%TFDqHE@6pk8>j_(#b2>~8W*#~XkyjL9 zXkbuD01`leZahdFr0l=^`Lat(R{JiPdg-jT4*ORP0fq*>3Wg2_7LYCmh5#Ujcmc!% z=>W+FKxHLBvS4wL3Cl{I29uS*F-$lO0qp^&A&?bJ;B*8^ zlOWr-%)199VPOe!Bgj3{FB%#mBwqcVvozJYsc7fb`p&LSkT|w9)NoIK<_o90j39Xi z21StuP9@v zvuj31&P%V_5nQlq)SP3k*(Y|V4Yd1WOlZi52j1K6w%TzqlYwXTI;)PEcZO{{W}8`8 zQBc*D`n3y3e@uX9L*5|Oyq!0%8x>(~KJV4w^*^gUSIu3L6THXAhFOTcu~fxM&*Ahu zr-j&_Mis^Zpyi^Q*v^0VMD(b+VPE05#4_>x;OukzOr(zQZEr6;J=ffOQ{1b~{&DVo zZ;r34E_U72iWgltP3=t*_cnOHZIhPvNX3wLI5VvpNu`xj&;Y_yX|7f z78Dlw?A!bErZn%vE1w#h9lU+-&aR}9sp$BwENS$8v4(XW^Wixb^(9pl}O@SZvUGQn! z1Isu+tqBQWlfX3c^zjZ3kWBv%Q?Pr}zs*~f>d7p^!1+noYL>`ir8=5TU~%*;S~9Fzm`SpHdd0iOmTDuGL~~%T z%(7#u7pU}nczO)5Evb03S;DB8foznD{uv300OPl(+O&RrLlv`VL%iA09&1abBNY`D zKA(Z9NCs*Gji*?`yw|{ID$0HB&(!HjRAwKo|X^w0mz>WBEHONt`;;8D4{=J7EL;tLYkRD zzl^~OSiOI+7T!ev;D7e!xz^`-gI=Gl4qV zQgPGv4{pt=g&>Lw*4sjxGDGSNqYjvog?8sfsS`d4^S0FPY!6ivfD%fzF;Okkce&H|mF)gMkcrox2-YHug?&-7CBH zZ{8T={U#im(thOr3~~X$pUM3!#sk2geYn}hwW#K6pOjXM7ynptw}$tL7*Br1cKEXr zX^CVuOi8o4{Mm+(nFo}77FLSUmHMDSh)VQlv7P9VAcy=C_dlFpU;^OJl=y`|^MLCl zxFJ>z#skrsX}=T45t2zky-L0kI4)8S3>>BTjc8p;v{*_*dW0bY4rqRBNpj;szqawn z?FC~f&0|IFW>gUWVB%zMg2b_Z;Sr!6v~x{BE;u`2UC<0PSLYtw&a?|X03^Rc?HV8$ z3x3U-d>Z^(ri!J(H;b+ev|NAKQ-ETUljHoV0Rtx=^Ukjf4a>AYu@`vS{-W820Hczu zm;H-#?SqD=nU*>e4RB1X_s+k5xzDhI)kg{+yw=wP?O5Z@iB%6(v4%mrYL2_VYqq5l z?ciZV*8!UBNgez%qi&k#&U(p~=O5+L*~W!%z^pUh&O#QUEJs2U_%))1posn){u_M5 zuXCRc-ciJajq?w;0RaC=U4K}gJpH&|>fmcrVh!JXTh6}{pTKFJ(Q9Gl%w9q32WNhB zHKS`E&VNRsy)mz(d=e(v8sa5Z98NJ%%uLkuq@7!EzTm_G;OkOh=%Z!^Uj<|@?|Wy( z_#h!G(dJ98Kdci$VQnjM4A>*Kmk>c?^7BKc$k>uy*-Y8uR& zGOt9+v1X8llrk@9;fcOf^acQ56ZYgTU-@!rNP%UYZ?Vy=-Ew`&`GQ`6qf1|tU)*+K zfY*tjWrv1NwGS^3;`$1M84(2Fr;+*+_9aaQxuHDkzO=(!1mN>;&uL2E>3(O(-n#Ag z!Y|Lh*jPU0{BXP6e{#J~`jATjYl}`DD7fNp%=rwE4kO$y8?*noz1fS)71mq#L7NHP zq9a37P6r>gjxo9=U$2tIG ze+R+@KwmzNJJ*jZw+Jqq?tf#;i&NVEUjDhhl@(}S`K1;OEQmS0VXa&)?Ou85;lYxXz8l<*jNX1dKMCswj#Avk zEa7A0tQXWe3@vyRZgs6};%4AOF7o)w<&u?DP@H_9m4m6i#QH-@)~kz z>9?ikF@A-YDg%4l_P@a8rDQLV*T?ap9&dFn^51fx?XjY9J%YHr63P?Hj<)?ZXkU%H zRrk(DcAS4NDY+H=0SUG-{(#olKsFxzzJ7CF97g8|(LJyhiN{CyYq| zem-A5;y*5J+=qW$`@GMn7fhatV#N?VZaScmp?1N z&x2V4@Z0UHVSQS^Up6#lS9a3X9_`KK{3_`eI3-r!)eZh>w^zE~_>JnH=WgWokeP6@ ze3A3*fRqj&nIzh5yUO{M>z(r(ARP++%5z9(fqRkvyw`#}T~4hWR%rX#@ZoN=FU$F# zT50`y!eLjh!XIiMkImlk0{8)rF2ARK>HOX95&gXK<2JT1zaJ%>+Yl!v{IoNDdRB#h zYUHAX8pqGx;rx3@hk`%O@8JRX+%KcjxY6C$3haD~BZXGHPToeC6k<+~~ z*BZ;eF8?Oyzn)L&kRb3$8>2Dj5L(MK?PI})B)%3dc|IwA#@67Rp2ov-TTVJ8=S#_- z0Y^#S*vE4Hal4lLInFQL00{6bc1`ot`*SlP&ly8!>j)~7=`5B^Wt<&L8%+0n z%jIe%9af?U2l2r1FUXU+VlD#gm#n*oR>3 zPaAiiH8tDV+%uWoxLwKpDCa*`I{f@`{sM>W(c~}U=xiTD&seSKF}S^SqZR=8@74dp z^?>V!A*H1=k4JtH+q1F!>+%{quY8r}$q<;B)Dj{;A6X&3y}l z9Ikv@8+BjKr_y<-Aw4VYBkT*go%4MmKUZY6AAN!pxdr=E$U5}V=SYzc^D9MRz4TH<0sl%-Odoxkng|IlQc;Qy z4bhAL?SWe9|Ec!Wj_eHvf*Oi8r2>WF>Jfs+I|PUtf@n?!4B;<1wN!Va8uX4-fagR7 z{mxX-j&mPsoPSVvp#nS?>D`rT;3MBbKjJ?65f^c)fCK1D1u?<7ZsBk^G~oB3f_}7* zK6fdDswQZXK$8S?NI=^M00sM|zR-ga!?$Yun>e3-;x|WDgr)b}HH`_ww@9GHcS!u3 zBz%tqub}@+Aj~5FY6)wDk3ivH%i)pdmDvA);wX)#W#}(=(tFsq=xI7_!P#zB@t}3I z($IHu8VzvL2H93`iZr&gNN1B5Sr;<214sUU4BBy?l%E@1XsmSVn|datxaj5v|HX3x zKRx=;{TGa})sHQoYHW!|@rxKVL>rq~&_MV~&d{KQsHyi)0BxPhzuSPH#G>pgG@8@3 zsw;0bKVojUFF&=)`Q#F`D}8T=cFMcsSk@e4XC + diff --git a/corpus/table-edit/created/cold/read/page-000.xml b/corpus/table-edit/created/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..18e6e85052b8d934f3546717ebb4a168ee13528d --- /dev/null +++ b/corpus/table-edit/created/cold/read/page-000.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/table-edit/created/cold/read/payloads.json b/corpus/table-edit/created/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/table-edit/created/cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/table-edit/created/cold/run.json b/corpus/table-edit/created/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..e5de875f30992feac8b4454db76cd812a676a4de --- /dev/null +++ b/corpus/table-edit/created/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/tables", + "expected_pages": 1, + "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/table-edit/created/cold/scripts/cold.ps1 b/corpus/table-edit/created/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/table-edit/created/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/table-edit/created/cold/scripts/read.ps1 b/corpus/table-edit/created/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/table-edit/created/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/table-edit/created/cold/source.json b/corpus/table-edit/created/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..a2b298152ccd8c0d8d5c33b61065d236fe51e883 --- /dev/null +++ b/corpus/table-edit/created/cold/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1604, + "sha256": "b39b2e142e0adce0de37ad336f40c3e9c2ba92ce6927c9d109139e74c1fb4a6e", + "mtime_ns": 1789239342755220465 + }, + { + "path": "tables.one", + "bytes": 11016, + "sha256": "e1c014220e05352a01c98752031b32c17512407295aa812ff8e9c776e48eef6e", + "mtime_ns": 1789239342754903465 + } +] diff --git a/corpus/table-edit/created/cold/teardown.json b/corpus/table-edit/created/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/table-edit/created/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/corpus/table-edit/edited/candidate/Open Notebook.onetoc2 b/corpus/table-edit/edited/candidate/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..7baa1ed5468231cbec163f9cbdee4219dac644d7 GIT binary patch literal 1612 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 A>Hq)$ literal 0 HcmV?d00001 diff --git a/corpus/table-edit/edited/cold/commands.jsonl b/corpus/table-edit/edited/cold/commands.jsonl new file mode 100644 index 0000000000000000000000000000000000000000..c96d0f091be95e979b634be55d13b6018adfdf29 --- /dev/null +++ b/corpus/table-edit/edited/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-C8AB6EC4 -ExpectedPages 12", "exit": 0, "stdout": "Read 1 sections and 12 pages.\r\n", "stderr": "", "error": null} +{"command": "powershell -NoProfile -Command \"Compress-Archive -Force -Path C:\\one-tests\\runs\\capture\\* -DestinationPath C:\\one-tests\\captured.zip\"", "exit": 0, "stdout": "", "stderr": "", "error": null} diff --git a/corpus/table-edit/edited/cold/machine.json b/corpus/table-edit/edited/cold/machine.json new file mode 100644 index 0000000000000000000000000000000000000000..366a1bac6efdfeffcb5c436ce8337ca91f48f7a4 --- /dev/null +++ b/corpus/table-edit/edited/cold/machine.json @@ -0,0 +1 @@ +{"name": "m6-c8ab6ec4", "hostname": "ONE-M6-C8AB6EC4"} diff --git a/corpus/table-edit/edited/cold/notebook/Open Notebook.onetoc2 b/corpus/table-edit/edited/cold/notebook/Open Notebook.onetoc2 new file mode 100644 index 0000000000000000000000000000000000000000..5b242159bcdd17896cdd58cc0860ae14ab0a2a32 GIT binary patch literal 3780 zcmZ3e|KIuM`!b(-j|5(YE0x9T8DuTjcPew+bUJ*E&h3tc3~0dq?koZ6iC;aJUJ#%7 z_Qbjy(m;7+3{(Ce2taHmm;jW9iK7X_xho4_Rlqqwh5{Rervs!8utL}gKni3I2!KS6 z@PSAK*b-Flb5&W}d7C(=O8vv-o0y?e!oJ5&J$`cBp|?NgqkiXM7laP%tSy=g`C%Bl zic!(g5E$|y(7?hlMV3KkrwoHgHjv&2qy>PU`m68Bad%s(|MJ~epR7snVP^!2$pCRN zLnT8VLkU9$Ln@HWWJqSvW5@@Jqypt&Ig)_^sJej*NH8*dlVMN+>4ktJU%r3cSQWNJ z5Xu)RU}#u;>$08xy>+f@`}QB?klXwords+1L&Jgz3WEOPT#ij9DT}Hz+0Me`VCJFI z5P3xrh6V3Wg2_7LYCmh6o^r zcmc!%=>W+_KxHLBvS4wL3%@rVVY-#Dk62Hr@ zobED$0R7nJvDdN!0)%4zd`;25A9d4S|M+9-yA= z%Stb21{nJ;YF7B9x!L(RNY2rsp=Sv&8L)5xb+PIOH1zlYdEBsM0yLl{g`o#I;DMSL z7-}>be#roh1#?D;(GVC70cZ#yXBAi(2+H@OKn%)onsYy7K$VVCqaiRF0wW*<01rPu A)c^nh literal 0 HcmV?d00001 diff --git a/corpus/table-edit/edited/cold/notebook/synthetic.one b/corpus/table-edit/edited/cold/notebook/synthetic.one new file mode 100644 index 0000000000000000000000000000000000000000..abc6b7d2afe3336b3dfc818271a373bf10afc416 GIT binary patch literal 96016 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 R*-4|J*niwfby0Es|3620>7M`q literal 0 HcmV?d00001 diff --git a/corpus/table-edit/edited/cold/read/environment.json b/corpus/table-edit/edited/cold/read/environment.json new file mode 100644 index 0000000000000000000000000000000000000000..55f0d8d4eaf69da26d29cd1a5ffdb5832cd4431d --- /dev/null +++ b/corpus/table-edit/edited/cold/read/environment.json @@ -0,0 +1,7 @@ +{ + "powershell": "5.1.14409.1005", + "schema": "xs2010", + "hostname": "ONE-M6-C8AB6EC4", + "cold": true, + "onenote": "14.0.4763.1000" +} diff --git a/corpus/table-edit/edited/cold/read/hierarchy.xml b/corpus/table-edit/edited/cold/read/hierarchy.xml new file mode 100644 index 0000000000000000000000000000000000000000..14baf191414f72df567a651ea2366eabdb127935 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/hierarchy.xml @@ -0,0 +1,2 @@ + + diff --git a/corpus/table-edit/edited/cold/read/page-000.xml b/corpus/table-edit/edited/cold/read/page-000.xml new file mode 100644 index 0000000000000000000000000000000000000000..7e01ef5eccb26f3b93e8aa93527be85d602efd1a --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-000.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/table-edit/edited/cold/read/page-001.xml b/corpus/table-edit/edited/cold/read/page-001.xml new file mode 100644 index 0000000000000000000000000000000000000000..ccd8cbe4adb0497888439ed7329e3d0591d28d13 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-001.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/table-edit/edited/cold/read/page-002.xml b/corpus/table-edit/edited/cold/read/page-002.xml new file mode 100644 index 0000000000000000000000000000000000000000..a194a97ac55a36f0fd1b2fbea947eac751bb6732 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-002.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/table-edit/edited/cold/read/page-003.xml b/corpus/table-edit/edited/cold/read/page-003.xml new file mode 100644 index 0000000000000000000000000000000000000000..3b054566709aa4f70f762969e387c38a9dfe81f6 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-003.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/table-edit/edited/cold/read/page-004.xml b/corpus/table-edit/edited/cold/read/page-004.xml new file mode 100644 index 0000000000000000000000000000000000000000..c63730b827fb33abf24bcd20c120100faa5fc2e4 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-004.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/table-edit/edited/cold/read/page-005.xml b/corpus/table-edit/edited/cold/read/page-005.xml new file mode 100644 index 0000000000000000000000000000000000000000..8a87fcf7d3d1bad53e7b9c077ede4c5b74bda386 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-005.xml @@ -0,0 +1,20 @@ + +Bold 🦀 italic é color 東京
+End
]]>
Fictitious: café, 東京, مرحبا]]>
diff --git a/corpus/table-edit/edited/cold/read/page-006.xml b/corpus/table-edit/edited/cold/read/page-006.xml new file mode 100644 index 0000000000000000000000000000000000000000..674f336f3956bf269378bf7ef9bdb89c8709c804 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-006.xml @@ -0,0 +1,4 @@ + +Target 🦀 é Link label]]> diff --git a/corpus/table-edit/edited/cold/read/page-007.xml b/corpus/table-edit/edited/cold/read/page-007.xml new file mode 100644 index 0000000000000000000000000000000000000000..51370350597d3babe05014eab64f81eb727bb9c4 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-007.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/table-edit/edited/cold/read/page-008.xml b/corpus/table-edit/edited/cold/read/page-008.xml new file mode 100644 index 0000000000000000000000000000000000000000..ea632e15e318bae07fe9b8d0dc3917e67b0cf274 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-008.xml @@ -0,0 +1,4 @@ + +Target 🦀 é Link label]]> diff --git a/corpus/table-edit/edited/cold/read/page-009.xml b/corpus/table-edit/edited/cold/read/page-009.xml new file mode 100644 index 0000000000000000000000000000000000000000..33525ee6752bd754e338a24907f11d49a23c2f45 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-009.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/table-edit/edited/cold/read/page-010.xml b/corpus/table-edit/edited/cold/read/page-010.xml new file mode 100644 index 0000000000000000000000000000000000000000..460ca52aa917dcf42070a98cc36eadc974d9cb8d --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-010.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/table-edit/edited/cold/read/page-011.xml b/corpus/table-edit/edited/cold/read/page-011.xml new file mode 100644 index 0000000000000000000000000000000000000000..95dbb17b8e9fac2d4fbfe20cefd2ac2bc9c3c7d6 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/page-011.xml @@ -0,0 +1,5 @@ + +Target 🦀 é Link label]]>Child]]> diff --git a/corpus/table-edit/edited/cold/read/payloads.json b/corpus/table-edit/edited/cold/read/payloads.json new file mode 100644 index 0000000000000000000000000000000000000000..1ce91a413d6fff4d58765187b76756f0e97ae843 --- /dev/null +++ b/corpus/table-edit/edited/cold/read/payloads.json @@ -0,0 +1,3 @@ +[ + +] \ No newline at end of file diff --git a/corpus/table-edit/edited/cold/run.json b/corpus/table-edit/edited/cold/run.json new file mode 100644 index 0000000000000000000000000000000000000000..25b225e714721fa0f1e8ca0f82c9dcf4f988b9ad --- /dev/null +++ b/corpus/table-edit/edited/cold/run.json @@ -0,0 +1,18 @@ +{ + "notebook": "/private/tmp/m4/table-edit", + "expected_pages": 12, + "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/table-edit/edited/cold/scripts/cold.ps1 b/corpus/table-edit/edited/cold/scripts/cold.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..a0b99e7aeb86f4dc10032537800501a52ad65b4f --- /dev/null +++ b/corpus/table-edit/edited/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/table-edit/edited/cold/scripts/read.ps1 b/corpus/table-edit/edited/cold/scripts/read.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..476094ab1e0c47077988a6461db2080cf208642b --- /dev/null +++ b/corpus/table-edit/edited/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/table-edit/edited/cold/source.json b/corpus/table-edit/edited/cold/source.json new file mode 100644 index 0000000000000000000000000000000000000000..b8b914af1aabb8a82f77a0a2f06c0af0a523f348 --- /dev/null +++ b/corpus/table-edit/edited/cold/source.json @@ -0,0 +1,14 @@ +[ + { + "path": "Open Notebook.onetoc2", + "bytes": 1612, + "sha256": "f49128a77aa74d62e6a1b1281abf6fd76f0789bf90cf47d812162404995c4276", + "mtime_ns": 1789239343363554023 + }, + { + "path": "synthetic.one", + "bytes": 92288, + "sha256": "1d0632852ecbf66bff16e9b36e2dfa3df059bdfd15807f4943bfeabda0e920ce", + "mtime_ns": 1789239343362662188 + } +] diff --git a/corpus/table-edit/edited/cold/teardown.json b/corpus/table-edit/edited/cold/teardown.json new file mode 100644 index 0000000000000000000000000000000000000000..05a47793de40f322e745c4c4183727e3590ecf70 --- /dev/null +++ b/corpus/table-edit/edited/cold/teardown.json @@ -0,0 +1 @@ +{"absent": true} diff --git a/crates/notebook/README.md b/crates/notebook/README.md index 1d1ae9d4f51e9c682863df7666f5493b7cd69975..3d4db5159b4d900edd52c2e189a7583132f6cb7f 100644 --- a/crates/notebook/README.md +++ b/crates/notebook/README.md @@ -14,7 +14,7 @@ diffs the supplied model against the page stored in the supplied local snapshot, writes the difference into the working image and queues one `Operation::Page(PageIntent { before, after, author })`; `before` is the page the edit started from and is the precondition reconciliation checks. Text, styles, -paragraph formatting, hyperlinks, bullets, numbering and note tags, paragraph structure, +paragraph formatting, hyperlinks, bullets, numbering, note tags, table rows and columns, paragraph structure, outline layout, insertions and deletions are all differences between `before` and `after`; the library never sees editor operations. diff --git a/crates/onestore/README.md b/crates/onestore/README.md index 8c58b5d6df89604fd4ee49f3d91bb06f82c6980e..ee8a2d030c56a902c6e51c434e892dfd289f815e 100644 --- a/crates/onestore/README.md +++ b/crates/onestore/README.md @@ -57,7 +57,7 @@ harness also accepts `--client-profile release`. | `ParagraphJoin`, `PreparedEdit::join` | Join adjacent ordinary text while preserving inherited character styles and native text-identity rules | | `TextAttribute`, `PreparedEdit::format` | Change character formatting over a UTF-16 range while sharing immutable styles; preserve unselected runs | | `OutlineEdit`, `PreparedEdit::outline` | Change ordinary outline position/width or a paragraph's saved expansion default, preserving identities and content | -| `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks, bullets and numbering, note tags, 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 | +| `PreparedEdit::page` | Publish an edited `page::Page` as one revision per changed space: text, character and paragraph formatting, hyperlinks, bullets and numbering, note tags, table rows and columns, 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 | | `TreeEdit`, `PreparedEdit::tree` | Move or delete a subtree on one page, normalize surviving containers, and replace an emptied table cell's paragraph atomically | | `PreparedEdit::commit`, `PreparedEdit::commit_file` | Publish the exact prepared image under caller-held exclusion or the conservative filesystem adapter | | `read_file` | Read a snapshot under whole-file exclusion | diff --git a/crates/onestore/src/page/write.rs b/crates/onestore/src/page/write.rs index c69cc31bf9a1d6b74f23d6306950cb9ff58d826c..099509b4a23b96415abd83b85dfb730c1263a620 100644 --- a/crates/onestore/src/page/write.rs +++ b/crates/onestore/src/page/write.rs @@ -1,7 +1,7 @@ //! Publishes an edited page model by lowering the difference from the stored page onto //! the typed writers, then squashing their transactions into one revision per space. -use super::{Outline, Page, PageObject, PageParagraph, ParagraphContent}; +use super::{Outline, Page, PageObject, PageParagraph, ParagraphContent, Table}; use crate::{ Error, ExGuid, Insertion, ObjectData, OutlineEdit, ParagraphJoin, ParagraphSplit, PropertySets, RevisionIndex, Store, TextAttribute, TreeEdit, Value, @@ -20,6 +20,50 @@ fn invalid(message: &'static str) -> Error { Error { offset: 0, message } } +/// A native measurement array: a count byte, `header - 1` reserved bytes, then each value +/// in inches. +fn measurement_bytes(values: &[f32], header: usize) -> Result, Error> { + let count = u8::try_from(values.len()) + .map_err(|_| invalid("A measurement array exceeds the document range"))?; + let mut bytes = vec![0u8; header]; + bytes[0] = count; + for value in values { + if !value.is_finite() { + return Err(invalid("A measurement must be finite")); + } + bytes.extend_from_slice(&(value / 36.0).to_le_bytes()); + } + Ok(bytes) +} + +/// Every row carries one cell per column and widths are usable. +fn validate_table(table: &Table) -> Result<(), Error> { + if table.rows.is_empty() || table.columns.is_empty() { + return Err(invalid("A table needs at least one row and one column")); + } + if table + .columns + .iter() + .any(|c| !c.width.is_finite() || c.width < 36.0) + { + return Err(invalid("Table columns are at least 36 points wide")); + } + for row in &table.rows { + if row.cells.len() != table.columns.len() { + return Err(invalid("Every table row has one cell per column")); + } + for cell in &row.cells { + // An emptied cell keeps a replacement paragraph, as the tree writer provides. + for paragraph in &cell.paragraphs { + if let ParagraphContent::Table(nested) = ¶graph.content { + validate_table(nested)?; + } + } + } + } + Ok(()) +} + pub(crate) fn write_page( source: &[u8], space: ExGuid, @@ -154,6 +198,16 @@ impl Lowering<'_> { self.alias.get(&model).copied().unwrap_or(model) } + /// A writer identity for a new object; squash renames it to the model's. + fn allocate(&mut self, model: ExGuid) -> Result { + let written = ExGuid { + guid: crate::write::fresh_guid()?, + n: 1, + }; + self.alias.insert(model, written); + Ok(written) + } + fn apply(&mut self, edit: impl FnOnce(&[u8]) -> Result, Error>) -> Result<(), Error> { self.image = edit(&self.image)?; Ok(()) @@ -187,6 +241,11 @@ impl Lowering<'_> { } let old = View::new(before)?; let new = View::new(after)?; + for paragraph in new.paragraphs.values() { + if let ParagraphContent::Table(table) = ¶graph.content { + validate_table(table)?; + } + } self.check_fixed_objects(before, after, &old, &new)?; for id in new.outlines.keys().chain(new.paragraphs.keys()) { if !old.outlines.contains_key(id) @@ -295,31 +354,28 @@ impl Lowering<'_> { } } (ParagraphContent::Table(table), ParagraphContent::Table(previous)) => { - let same = table.id == previous.id - && table.columns == previous.columns - && table.borders == previous.borders - && table.layout == previous.layout - && table.tags == previous.tags - && table.rows.len() == previous.rows.len() - && table - .rows - .iter() - .zip(&previous.rows) - .all(|(row, previous)| { - row.id == previous.id - && row.cells.len() == previous.cells.len() - && row.cells.iter().zip(&previous.cells).all( - |(cell, previous)| { - cell.id == previous.id - && cell.layout == previous.layout - && cell.indents == previous.indents - && cell.shading == previous.shading - && cell.unsupported == previous.unsupported - }, - ) - }); - if !same { - return Err(invalid("Table structure cannot be edited")); + if table.id != previous.id + || table.layout != previous.layout + || table.tags != previous.tags + { + return Err(invalid("Table identity, layout and tags cannot be edited")); + } + let unchanged_cells = + table.rows.iter().flat_map(|row| &row.cells).all(|cell| { + previous + .rows + .iter() + .flat_map(|row| &row.cells) + .find(|before| before.id == cell.id) + .is_none_or(|before| { + cell.layout == before.layout + && cell.indents == before.indents + && cell.shading == before.shading + && cell.unsupported == before.unsupported + }) + }); + if !unchanged_cells { + return Err(invalid("Cell layout, indents and shading cannot be edited")); } } (ParagraphContent::Unsupported(a), ParagraphContent::Unsupported(b)) if a == b => {} @@ -544,10 +600,30 @@ impl Lowering<'_> { collect_containers(&outline.paragraphs, &mut containers); } } + // Cells that do not exist yet receive their paragraphs once the table structure does. + let (existing, deferred): (Vec, Vec) = + containers.into_iter().partition(|container| { + old.children.contains_key(container) + || new.paragraphs.contains_key(container) + || new.outlines.contains_key(container) + }); + self.place_containers(old, new, placed, &existing)?; + self.edit_table_structure(old, new)?; + self.place_containers(old, new, placed, &deferred)?; + Ok(()) + } + + fn place_containers( + &mut self, + old: &View<'_>, + new: &View<'_>, + placed: &BTreeMap>, + containers: &[ExGuid], + ) -> Result<(), Error> { for container in containers { - let after = &new.children[&container]; + let after = &new.children[container]; let current: Vec = placed - .get(&container) + .get(container) .map(|list| { list.iter() .copied() @@ -561,26 +637,35 @@ impl Lowering<'_> { let anchor = next.map(|n| self.id(n)); if !old.paragraphs.contains_key(id) && !self.alias.contains_key(id) { let paragraph = new.paragraphs[id]; - let text = paragraph - .text() - .filter(|text| text.date_field.is_none()) - .filter(|_| paragraph.style.is_none()) - .ok_or_else(|| { - invalid("New paragraphs contain plain text without fields or styles") - })?; - let insertion = Insertion::paragraph( - self.id(container), - anchor, - text.text.text(), - self.author, - )?; + if paragraph.style.is_some() { + return Err(invalid( + "New paragraphs contain plain text without fields or styles", + )); + } + // A new table starts as an empty text paragraph whose content the + // structure pass replaces with the table. + let (text, text_id) = match ¶graph.content { + ParagraphContent::Text(text) if text.date_field.is_none() => { + (text.text.text(), Some(text.id)) + } + ParagraphContent::Table(_) => ("", None), + _ => { + return Err(invalid( + "New paragraphs contain plain text without fields or styles", + )); + } + }; + let insertion = + Insertion::paragraph(self.id(*container), anchor, text, self.author)?; let space = self.space; self.apply(|image| insertion.apply(image, space))?; self.alias.insert(*id, insertion.object()); - self.alias.insert(text.id, insertion.text_object()); + if let Some(text_id) = text_id { + self.alias.insert(text_id, insertion.text_object()); + } } else if !kept.contains(id) { let edit = - TreeEdit::move_to(self.id(*id), self.id(container), anchor, self.author)?; + TreeEdit::move_to(self.id(*id), self.id(*container), anchor, self.author)?; let space = self.space; self.apply(|image| edit.apply(image, space))?; } @@ -590,6 +675,187 @@ impl Lowering<'_> { Ok(()) } + /// Creates tables, rows and cells the model added, rebuilds every table's row and cell + /// order to the model's, and writes the column widths, locks and border flag. Native + /// rows and cells are plain containers; a cell carries its indent array and the flags + /// every native cell has. + fn edit_table_structure(&mut self, old: &View<'_>, new: &View<'_>) -> Result<(), Error> { + let old_tables: BTreeMap = old + .paragraphs + .values() + .filter_map(|p| match &p.content { + ParagraphContent::Table(table) => Some((table.id, table)), + _ => None, + }) + .collect(); + for (paragraph_id, paragraph) in &new.paragraphs { + let ParagraphContent::Table(table) = ¶graph.content else { + continue; + }; + let previous = old_tables.get(&table.id).copied(); + let unchanged = previous.is_some_and(|previous| { + previous.columns == table.columns + && previous.borders == table.borders + && previous.rows.len() == table.rows.len() + && previous.rows.iter().zip(&table.rows).all(|(a, b)| { + a.id == b.id + && a.cells.len() == b.cells.len() + && a.cells.iter().zip(&b.cells).all(|(x, y)| x.id == y.id) + }) + }); + if unchanged { + continue; + } + let modified = crate::create::current_timestamps()?.0.to_le_bytes(); + let mut created: Vec<(ExGuid, u32, Values)> = Vec::new(); + let table_id = if previous.is_some() { + self.id(table.id) + } else { + self.allocate(table.id)? + }; + let existing_rows: BTreeSet = previous + .map(|p| p.rows.iter().map(|r| r.id).collect()) + .unwrap_or_default(); + let existing_cells: BTreeSet = previous + .map(|p| { + p.rows + .iter() + .flat_map(|r| r.cells.iter().map(|c| c.id)) + .collect() + }) + .unwrap_or_default(); + let template = previous + .and_then(|p| p.rows.first()) + .and_then(|r| r.cells.first()); + let mut rows = Vec::new(); + for row in &table.rows { + let row_id = if existing_rows.contains(&row.id) { + self.id(row.id) + } else { + self.allocate(row.id)? + }; + let mut cells = Vec::new(); + for cell in &row.cells { + let cell_id = if existing_cells.contains(&cell.id) { + self.id(cell.id) + } else { + let written = self.allocate(cell.id)?; + let indents = if cell.indents.is_empty() { + template.map(|t| t.indents.clone()).unwrap_or_default() + } else { + cell.indents.clone() + }; + let mut values: Values = vec![ + (0x14001d7a, modified.to_vec()), + (0x0c001c13, vec![0]), + (0x0c001c03, vec![1]), + (0x88001c91, Vec::new()), + ]; + if !indents.is_empty() { + values.push((0x1c001c12, measurement_bytes(&indents, 4)?)); + } + if let Some(shading) = cell.shading { + values.push((0x14001e26, shading.to_le_bytes().to_vec())); + } + created.push((written, 0x60024, values)); + written + }; + cells.push(cell_id); + } + if !existing_rows.contains(&row.id) { + created.push((row_id, 0x60023, vec![(0x14001d7a, modified.to_vec())])); + } + rows.push((row_id, cells)); + } + let mut table_values: Values = vec![ + (0x14001d7a, modified.to_vec()), + (0x14001d57, (table.rows.len() as u32).to_le_bytes().to_vec()), + ( + 0x14001d58, + (table.columns.len() as u32).to_le_bytes().to_vec(), + ), + ( + 0x1c001d66, + measurement_bytes( + &table.columns.iter().map(|c| c.width).collect::>(), + 1, + )?, + ), + ]; + let mut locks = vec![table.columns.len() as u8]; + locks.extend(vec![0u8; table.columns.len().div_ceil(8)]); + for (i, column) in table.columns.iter().enumerate() { + if column.locked { + locks[1 + i / 8] |= 1 << (i % 8); + } + } + table_values.push((0x1c001d7d, locks)); + table_values.push(( + 0x08001d5e | (u32::from(table.borders.unwrap_or(true)) << 31), + Vec::new(), + )); + if previous.is_none() { + table_values.push((0x14001c3e, 1u32.to_le_bytes().to_vec())); + table_values.push((0x14001c84, 1u32.to_le_bytes().to_vec())); + created.push((table_id, 0x60022, Vec::new())); + } + let holder = self.id(*paragraph_id); + let is_new = previous.is_none(); + let space = self.space; + self.apply(|image| { + crate::write::write_revision(image, space, |raw| { + let mut changed: BTreeMap = BTreeMap::new(); + for (id, jcid, values) in &created { + let mut node = PropertyObject { + jcid: *jcid, + bytes: crate::create::properties(values)?, + global_ids: std::sync::Arc::new(BTreeMap::from([(0, id.guid)])), + }; + node.reference(*id)?; + changed.insert(*id, node); + } + let mut row_refs = Vec::new(); + for (row_id, cells) in &rows { + let mut row = match changed.remove(row_id) { + Some(row) => row, + None => PropertyObject::from_object(&raw.objects[row_id])?, + }; + let mut cell_refs = Vec::new(); + for cell in cells { + cell_refs.extend_from_slice(&row.reference(*cell)?); + } + row.set(&[(0x24001c20, &cell_refs), (0x14001d7a, &modified)])?; + changed.insert(*row_id, row); + } + let mut table_object = match changed.remove(&table_id) { + Some(object) => object, + None => PropertyObject::from_object(&raw.objects[&table_id])?, + }; + for (row_id, _) in &rows { + row_refs.extend_from_slice(&table_object.reference(*row_id)?); + } + table_object.remove(&[0x1c001d7d, 0x08001d5e])?; + table_object.set( + &table_values + .iter() + .map(|(id, bytes)| (*id, bytes.as_slice())) + .collect::>(), + )?; + table_object.set(&[(0x24001c20, &row_refs)])?; + changed.insert(table_id, table_object); + if is_new { + let mut object = PropertyObject::from_object(&raw.objects[&holder])?; + let reference = object.reference(table_id)?; + object.set(&[(0x24001c1f, &reference), (0x14001d7a, &modified)])?; + changed.insert(holder, object); + } + Ok(changed) + }) + })?; + } + Ok(()) + } + fn delete( &mut self, old: &View<'_>, @@ -609,6 +875,10 @@ impl Lowering<'_> { || (old.paragraphs.contains_key(&ancestor) && !new.paragraphs.contains_key(&ancestor) && !consumed.contains(&ancestor)) + || (old.children.contains_key(&ancestor) + && !old.paragraphs.contains_key(&ancestor) + && !old.outlines.contains_key(&ancestor) + && !new.children.contains_key(&ancestor)) { covered = true; break; diff --git a/crates/onestore/tests/page_tables.rs b/crates/onestore/tests/page_tables.rs new file mode 100644 index 0000000000000000000000000000000000000000..cf91ec2dbf79281159c6cf07428341f9483d4dbf --- /dev/null +++ b/crates/onestore/tests/page_tables.rs @@ -0,0 +1,306 @@ +use onestore::{ + ExGuid, PreparedEdit, RevisionIndex, Store, + document::{Document, Format}, + page::{ + Page, PageObject, PageParagraph, ParagraphContent, Table, TableCell, TableColumn, TableRow, + TextObject, text::new_id, + }, +}; + +const TREES: &[u8] = + include_bytes!("../../../corpus/outline-edit/tree/before/notebook/synthetic.one"); +const AUTHOR: &str = "Table author"; + +fn page_by_title(bytes: &[u8], title: &str) -> (ExGuid, Page) { + let store = Store::parse(bytes).unwrap(); + assert!(store.checksum_mismatches.is_empty()); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + document + .pages() + .unwrap() + .into_iter() + .find_map(|(space, _)| { + let page = Page::from_space(&document, space).unwrap(); + (page.title == title).then_some((space, page)) + }) + .unwrap() +} + +fn page_in(bytes: &[u8], space: ExGuid) -> Page { + let store = Store::parse(bytes).unwrap(); + assert!(store.checksum_mismatches.is_empty()); + let index = RevisionIndex::parse(&store).unwrap(); + Page::from_space(&Document::parse(&index).unwrap(), space).unwrap() +} + +fn body_paragraphs(page: &mut Page) -> &mut Vec { + page.objects + .iter_mut() + .find_map(|object| match object { + PageObject::Outline(outline) if !outline.title => Some(&mut outline.paragraphs), + _ => None, + }) + .unwrap() +} + +fn table_mut(page: &mut Page) -> &mut Table { + body_paragraphs(page) + .iter_mut() + .find_map(|p| match &mut p.content { + ParagraphContent::Table(table) => Some(table), + _ => None, + }) + .unwrap() +} + +/// Definitions are read back only while a paragraph references them. +fn referenced(page: &Page) -> std::collections::BTreeSet { + fn walk(paragraphs: &[PageParagraph], out: &mut std::collections::BTreeSet) { + for paragraph in paragraphs { + out.extend(paragraph.lists.iter().copied()); + out.extend(paragraph.style); + out.extend(paragraph.tags.iter().filter_map(|t| t.definition)); + match ¶graph.content { + ParagraphContent::Text(text) => { + out.extend(text.tags.iter().filter_map(|t| t.definition)); + } + ParagraphContent::Table(table) => { + for row in &table.rows { + for cell in &row.cells { + walk(&cell.paragraphs, out); + } + } + } + ParagraphContent::Unsupported(_) => {} + } + } + } + let mut out = std::collections::BTreeSet::new(); + for object in &page.objects { + match object { + PageObject::Outline(outline) => walk(&outline.paragraphs, &mut out), + PageObject::Title(title) => { + for outline in &title.outlines { + walk(&outline.paragraphs, &mut out); + } + } + _ => {} + } + } + out +} + +fn assert_same(written: &[u8], space: ExGuid, expected: &Page) -> Page { + let stored = page_in(written, space); + let mut expected = expected.clone(); + expected.title = stored.title.clone(); + let live = referenced(&expected); + expected.definitions.retain(|id, _| live.contains(id)); + assert_eq!(stored, expected); + stored +} + +fn plain_text(text: &str) -> TextObject { + TextObject { + id: new_id().unwrap(), + date_field: None, + text: onestore::page::Paragraph::new( + text.into(), + Format { + font: Some("Calibri".into()), + font_size: Some(11.0), + language: Some(1033), + ..Default::default() + }, + ), + tags: Vec::new(), + } +} + +fn cell_paragraph(template: &PageParagraph, text: &str) -> 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; + paragraph.content = ParagraphContent::Text(plain_text(text)); + paragraph +} + +fn new_cell(template: &TableCell, text: &str) -> TableCell { + TableCell { + id: new_id().unwrap(), + layout: Default::default(), + indents: template.indents.clone(), + shading: None, + paragraphs: vec![cell_paragraph(&template.paragraphs[0], text)], + unsupported: Vec::new(), + } +} + +#[test] +fn a_row_and_a_column_are_added_to_a_native_table_and_removed_again() { + let (space, before) = page_by_title(TREES, "Delete cell subtree"); + let mut after = before.clone(); + let table = table_mut(&mut after); + assert_eq!((table.rows.len(), table.columns.len()), (1, 2)); + let template = table.rows[0].cells[0].clone(); + table.columns.push(TableColumn { + width: 120.0, + locked: false, + }); + table.rows[0] + .cells + .push(new_cell(&template, "Third column")); + table.rows.push(TableRow { + id: new_id().unwrap(), + cells: vec![ + new_cell(&template, "Second row"), + new_cell(&template, "Second row, second column"), + new_cell(&template, "Second row, third column"), + ], + }); + let written = PreparedEdit::page(TREES, space, &after, AUTHOR).unwrap(); + let stored = assert_same(written.as_bytes(), space, &after); + assert_eq!( + PreparedEdit::page(written.as_bytes(), space, &stored, AUTHOR) + .unwrap() + .as_bytes(), + written.as_bytes() + ); + if let Some(directory) = std::env::var_os("ONESTORE_TABLE_EDIT_EXPORT") { + let directory = std::path::PathBuf::from(directory); + std::fs::create_dir(&directory).unwrap(); + std::fs::write(directory.join("synthetic.one"), written.as_bytes()).unwrap(); + let written_store = Store::parse(written.as_bytes()).unwrap(); + std::fs::write( + directory.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents( + "Open Notebook.onetoc2", + &[("synthetic.one", written_store.header.file_id)], + ) + .unwrap(), + ) + .unwrap(); + } + let mut smaller = stored.clone(); + let table = table_mut(&mut smaller); + table.columns.remove(1); + for row in &mut table.rows { + row.cells.remove(1); + } + table.rows.remove(0); + let again = PreparedEdit::page(written.as_bytes(), space, &smaller, AUTHOR).unwrap(); + let mut stored = assert_same(again.as_bytes(), space, &smaller); + let table = table_mut(&mut stored); + assert_eq!((table.rows.len(), table.columns.len()), (1, 2)); +} + +#[test] +fn column_widths_locks_and_borders_change_in_place() { + let (space, before) = page_by_title(TREES, "Delete cell subtree"); + let mut after = before.clone(); + let table = table_mut(&mut after); + table.columns[0].width = 300.0; + table.columns[1].locked = false; + table.borders = Some(false); + let written = PreparedEdit::page(TREES, space, &after, AUTHOR).unwrap(); + assert_same(written.as_bytes(), space, &after); +} + +#[test] +fn inconsistent_tables_are_refused() { + let (space, before) = page_by_title(TREES, "Delete cell subtree"); + let mut ragged = before.clone(); + let template = table_mut(&mut ragged).rows[0].cells[0].clone(); + table_mut(&mut ragged).rows[0] + .cells + .push(new_cell(&template, "Extra")); + assert!(PreparedEdit::page(TREES, space, &ragged, AUTHOR).is_err()); + let mut empty = before.clone(); + table_mut(&mut empty).rows[0].cells[0].paragraphs.clear(); + let written = PreparedEdit::page(TREES, space, &empty, AUTHOR).unwrap(); + let mut stored = page_in(written.as_bytes(), space); + let cell = &table_mut(&mut stored).rows[0].cells[0]; + assert_eq!(cell.paragraphs.len(), 1); + assert_eq!(cell.paragraphs[0].text().unwrap().text.text(), ""); + let mut narrow = before.clone(); + table_mut(&mut narrow).columns[0].width = 10.0; + assert!(PreparedEdit::page(TREES, space, &narrow, AUTHOR).is_err()); + let mut rowless = before.clone(); + table_mut(&mut rowless).rows.clear(); + assert!(PreparedEdit::page(TREES, space, &rowless, AUTHOR).is_err()); +} + +/// `ONESTORE_TABLE_EXPORT` names a new directory receiving the candidate for a cold reopen. +#[test] +fn a_new_table_is_created_on_a_fresh_page() { + let source = onestore::create_section("tables.one", "Before the table", "Author").unwrap(); + let store = Store::parse(&source).unwrap(); + let index = RevisionIndex::parse(&store).unwrap(); + let document = Document::parse(&index).unwrap(); + let (space, _) = document.pages().unwrap()[0]; + let before = Page::from_space(&document, space).unwrap(); + let mut after = before.clone(); + let template = body_paragraphs(&mut after)[0].clone(); + let cell = |text: &str| TableCell { + id: new_id().unwrap(), + layout: Default::default(), + indents: Vec::new(), + shading: None, + paragraphs: vec![cell_paragraph(&template, text)], + unsupported: Vec::new(), + }; + let table = Table { + id: new_id().unwrap(), + columns: vec![ + TableColumn { + width: 144.0, + locked: true, + }, + TableColumn { + width: 216.0, + locked: true, + }, + ], + rows: vec![ + TableRow { + id: new_id().unwrap(), + cells: vec![cell("Name"), cell("Value")], + }, + TableRow { + id: new_id().unwrap(), + cells: vec![cell("Rust 🦀"), cell("Written natively")], + }, + ], + borders: Some(true), + layout: Default::default(), + tags: Vec::new(), + }; + let mut holder = cell_paragraph(&template, ""); + holder.content = ParagraphContent::Table(table); + holder.format = Format::default(); + body_paragraphs(&mut after).push(holder); + body_paragraphs(&mut after).push(cell_paragraph(&template, "After the table")); + let written = PreparedEdit::page(&source, space, &after, AUTHOR).unwrap(); + assert_same(written.as_bytes(), space, &after); + if let Some(directory) = std::env::var_os("ONESTORE_TABLE_EXPORT") { + let directory = std::path::PathBuf::from(directory); + std::fs::create_dir(&directory).unwrap(); + std::fs::write(directory.join("tables.one"), written.as_bytes()).unwrap(); + let written_store = Store::parse(written.as_bytes()).unwrap(); + std::fs::write( + directory.join("Open Notebook.onetoc2"), + onestore::create_table_of_contents( + "Open Notebook.onetoc2", + &[("tables.one", written_store.header.file_id)], + ) + .unwrap(), + ) + .unwrap(); + } +} diff --git a/crates/onestore/tests/page_writer.rs b/crates/onestore/tests/page_writer.rs index f842bd485bb4d470740e955ba93fe34e3ed405c1..b7efc8395d6c50d52f9df30c10cc0705733d36c3 100644 --- a/crates/onestore/tests/page_writer.rs +++ b/crates/onestore/tests/page_writer.rs @@ -604,7 +604,7 @@ fn unsupported_model_edits_are_rejected_before_writing() { } } } - assert!(PreparedEdit::page(TREES, space, &widened, AUTHOR).is_err()); + assert!(PreparedEdit::page(TREES, space, &widened, AUTHOR).is_ok()); let mut relisted = page_in(TREES, space); let outline = body(&page)[0]; for object in &mut relisted.objects { diff --git a/tools/test_table_edit.py b/tools/test_table_edit.py new file mode 100644 index 0000000000000000000000000000000000000000..12305d45cde9b4fa729a8e4cbe9b154042d685b7 --- /dev/null +++ b/tools/test_table_edit.py @@ -0,0 +1,58 @@ +import html +from pathlib import Path +import re +import runpy +import shutil +from tempfile import TemporaryDirectory +import unittest +import xml.etree.ElementTree as ET + +from native_xml import ns + +ROOT = Path(__file__).resolve().parent.parent +FIXTURE = ROOT / 'corpus/table-edit' +compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] + + +def tables(read, title): + for path in sorted(read.glob('page-*.xml')): + page = ET.parse(path).getroot() + if page.get('name') != title: + continue + out = [] + for table in page.iter('{%s}Table' % ns['one']): + columns = [(column.get('width'), column.get('isLocked')) for column in table.find('one:Columns', ns)] + rows = [[[html.unescape(re.sub(r'<[^>]*>', '', text.text or '')) for text in cell.findall('.//one:T', ns)] + for cell in row.findall('one:Cell', ns)] for row in table.findall('one:Row', ns)] + out.append((table.get('bordersVisible'), columns, rows)) + return out + raise AssertionError(title) + + +class TableEditTest(unittest.TestCase): + def check(self, name): + with TemporaryDirectory() as temporary: + read = Path(temporary) / 'read' + shutil.copytree(FIXTURE / name / 'cold/read', read) + compare(FIXTURE / name / 'candidate', read) + + def test_a_rust_table_renders_natively(self): + self.check('created') + self.assertEqual(tables(FIXTURE / 'created/cold/read', 'Before the table'), [ + ('true', [('144.0', 'true'), ('216.0', 'true')], + [[['Name'], ['Value']], [['Rust 🦀'], ['Written natively']]]), + ]) + + def test_rows_and_columns_added_to_a_native_table_render_natively(self): + self.check('edited') + (borders, columns, rows), = tables(FIXTURE / 'edited/cold/read', 'Delete cell subtree') + self.assertEqual(borders, 'true') + self.assertEqual(columns[:2], [('240.0', 'true'), ('180.0', 'true')]) + self.assertEqual(len(columns), 3) + self.assertIsNone(columns[2][1]) + self.assertEqual(rows[0][2], ['Third column']) + self.assertEqual(rows[1], [['Second row'], ['Second row, second column'], ['Second row, third column']]) + + +if __name__ == '__main__': + unittest.main() -- 2.54.0