| author | |
| committer | |
| log | 021a7246820757dd105aa45223eec7b9add63f79 |
| tree | 4cd305e86edfb33ec9c929115ff04635ff8c1004 |
| parent | 24d94846b5b33c2a832f3814bf0d80cf7a66cdde |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Word writes list items as paragraphs whose mso-list style names the list
and level, with the marker in a mso-list:Ignore span, so they pasted as
plain paragraphs. They now paste as OneNote 2010 pasted the same copy
(lab, 2026-10-02): each level nested under the item above, a bullet in
Word's glyph and font without a gallery index, a number in Word's
sequence and punctuation. A list without a number now copies out as a
ul. LibreOffice's nested ul and ol already pasted as lists; a test keeps
them so. Corpus row word-lists holds OneNote's cold read.
Fixes #61.
Assisted-by: claude-opus-5.524 files changed, 1285 insertions(+), 20 deletions(-)
corpus/clipboard/README.md+14| ... | @@ -9,3 +9,17 @@ built through COM and copied with Ctrl+A, Ctrl+C). Beside it OneNote offered | ... | @@ -9,3 +9,17 @@ built through COM and copied with Ctrl+A, Ctrl+C). Beside it OneNote offered |
| 9 | ANSI text, an enhanced metafile and a device-independent bitmap, but no RTF. The | 9 | ANSI text, an enhanced metafile and a device-independent bitmap, but no RTF. The |
| 10 | tag is absent from the HTML. `crates/canvas/src/editor/html.rs` reads it in its | 10 | tag is absent from the HTML. `crates/canvas/src/editor/html.rs` reads it in its |
| 11 | tests. | 11 | tests. |
| 12 | |||
| 13 | `word-2010.html` is the `HTML Format` Word 2010 put on the clipboard for a | ||
| 14 | document of a bulleted list three levels deep, a plain paragraph, a numbered | ||
| 15 | list three levels deep (1., a., i.) and a last plain paragraph, typed with | ||
| 16 | AutoFormat and Tab and copied with Ctrl+A, Ctrl+C (lab, 2026-10-02). Word | ||
| 17 | writes each item as a paragraph whose `mso-list` style names its list and | ||
| 18 | level, with its marker in a `mso-list:Ignore` span inside `<![if | ||
| 19 | !supportLists]>`. Pasted into OneNote 2010, each level nested under the item | ||
| 20 | above; bullets kept Word's glyph and font (Symbol `·`, Courier New `o`, | ||
| 21 | Wingdings `§`) with no gallery index, and numbers took Word's sequence and | ||
| 22 | punctuation. OneNote also nested the numbered list under the plain paragraph | ||
| 23 | before it, from Word's indents; Snowbound nests by `mso-list` level alone, as | ||
| 24 | it reads `ul` and `ol`. `crates/canvas/src/editor/html.rs` and | ||
| 25 | `crates/canvas/tests/clip_paste.rs` read it. |
corpus/clipboard/word-2010.html created+691| ... | @@ -0,0 +1,691 @@ | ||
| 1 | Version:1.0 | ||
| 2 | StartHTML:0000000105 | ||
| 3 | EndHTML:0000031043 | ||
| 4 | StartFragment:0000027392 | ||
| 5 | EndFragment:0000031003 | ||
| 6 | |||
| 7 | <html xmlns:o="urn:schemas-microsoft-com:office:office" | ||
| 8 | xmlns:w="urn:schemas-microsoft-com:office:word" | ||
| 9 | xmlns:m="http://schemas.microsoft.com/office/2004/12/omml" | ||
| 10 | xmlns="http://www.w3.org/TR/REC-html40"> | ||
| 11 | |||
| 12 | <head> | ||
| 13 | <meta http-equiv=Content-Type content="text/html; charset=utf-8"> | ||
| 14 | <meta name=ProgId content=Word.Document> | ||
| 15 | <meta name=Generator content="Microsoft Word 14"> | ||
| 16 | <meta name=Originator content="Microsoft Word 14"> | ||
| 17 | <link rel=File-List | ||
| 18 | href="file:///C:\Users\clover\AppData\Local\Temp\msohtmlclip1\01\clip_filelist.xml"> | ||
| 19 | <!--[if gte mso 9]><xml> | ||
| 20 | <o:OfficeDocumentSettings> | ||
| 21 | <o:AllowPNG/> | ||
| 22 | </o:OfficeDocumentSettings> | ||
| 23 | </xml><![endif]--> | ||
| 24 | <link rel=themeData | ||
| 25 | href="file:///C:\Users\clover\AppData\Local\Temp\msohtmlclip1\01\clip_themedata.thmx"> | ||
| 26 | <link rel=colorSchemeMapping | ||
| 27 | href="file:///C:\Users\clover\AppData\Local\Temp\msohtmlclip1\01\clip_colorschememapping.xml"> | ||
| 28 | <!--[if gte mso 9]><xml> | ||
| 29 | <w:WordDocument> | ||
| 30 | <w:View>Normal</w:View> | ||
| 31 | <w:Zoom>0</w:Zoom> | ||
| 32 | <w:TrackMoves/> | ||
| 33 | <w:TrackFormatting/> | ||
| 34 | <w:PunctuationKerning/> | ||
| 35 | <w:ValidateAgainstSchemas/> | ||
| 36 | <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid> | ||
| 37 | <w:IgnoreMixedContent>false</w:IgnoreMixedContent> | ||
| 38 | <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText> | ||
| 39 | <w:DoNotPromoteQF/> | ||
| 40 | <w:LidThemeOther>EN-US</w:LidThemeOther> | ||
| 41 | <w:LidThemeAsian>X-NONE</w:LidThemeAsian> | ||
| 42 | <w:LidThemeComplexScript>X-NONE</w:LidThemeComplexScript> | ||
| 43 | <w:Compatibility> | ||
| 44 | <w:BreakWrappedTables/> | ||
| 45 | <w:SnapToGridInCell/> | ||
| 46 | <w:WrapTextWithPunct/> | ||
| 47 | <w:UseAsianBreakRules/> | ||
| 48 | <w:DontGrowAutofit/> | ||
| 49 | <w:SplitPgBreakAndParaMark/> | ||
| 50 | <w:EnableOpenTypeKerning/> | ||
| 51 | <w:DontFlipMirrorIndents/> | ||
| 52 | <w:OverrideTableStyleHps/> | ||
| 53 | </w:Compatibility> | ||
| 54 | <m:mathPr> | ||
| 55 | <m:mathFont m:val="Cambria Math"/> | ||
| 56 | <m:brkBin m:val="before"/> | ||
| 57 | <m:brkBinSub m:val="&#45;-"/> | ||
| 58 | <m:smallFrac m:val="off"/> | ||
| 59 | <m:dispDef/> | ||
| 60 | <m:lMargin m:val="0"/> | ||
| 61 | <m:rMargin m:val="0"/> | ||
| 62 | <m:defJc m:val="centerGroup"/> | ||
| 63 | <m:wrapIndent m:val="1440"/> | ||
| 64 | <m:intLim m:val="subSup"/> | ||
| 65 | <m:naryLim m:val="undOvr"/> | ||
| 66 | </m:mathPr></w:WordDocument> | ||
| 67 | </xml><![endif]--><!--[if gte mso 9]><xml> | ||
| 68 | <w:LatentStyles DefLockedState="false" DefUnhideWhenUsed="true" | ||
| 69 | DefSemiHidden="true" DefQFormat="false" DefPriority="99" | ||
| 70 | LatentStyleCount="267"> | ||
| 71 | <w:LsdException Locked="false" Priority="0" SemiHidden="false" | ||
| 72 | UnhideWhenUsed="false" QFormat="true" Name="Normal"/> | ||
| 73 | <w:LsdException Locked="false" Priority="9" SemiHidden="false" | ||
| 74 | UnhideWhenUsed="false" QFormat="true" Name="heading 1"/> | ||
| 75 | <w:LsdException Locked="false" Priority="9" QFormat="true" Name="heading 2"/> | ||
| 76 | <w:LsdException Locked="false" Priority="9" QFormat="true" Name="heading 3"/> | ||
| 77 | <w:LsdException Locked="false" Priority="9" QFormat="true" Name="heading 4"/> | ||
| 78 | <w:LsdException Locked="false" Priority="9" QFormat="true" Name="heading 5"/> | ||
| 79 | <w:LsdException Locked="false" Priority="9" QFormat="true" Name="heading 6"/> | ||
| 80 | <w:LsdException Locked="false" Priority="9" QFormat="true" Name="heading 7"/> | ||
| 81 | <w:LsdException Locked="false" Priority="9" QFormat="true" Name="heading 8"/> | ||
| 82 | <w:LsdException Locked="false" Priority="9" QFormat="true" Name="heading 9"/> | ||
| 83 | <w:LsdException Locked="false" Priority="39" Name="toc 1"/> | ||
| 84 | <w:LsdException Locked="false" Priority="39" Name="toc 2"/> | ||
| 85 | <w:LsdException Locked="false" Priority="39" Name="toc 3"/> | ||
| 86 | <w:LsdException Locked="false" Priority="39" Name="toc 4"/> | ||
| 87 | <w:LsdException Locked="false" Priority="39" Name="toc 5"/> | ||
| 88 | <w:LsdException Locked="false" Priority="39" Name="toc 6"/> | ||
| 89 | <w:LsdException Locked="false" Priority="39" Name="toc 7"/> | ||
| 90 | <w:LsdException Locked="false" Priority="39" Name="toc 8"/> | ||
| 91 | <w:LsdException Locked="false" Priority="39" Name="toc 9"/> | ||
| 92 | <w:LsdException Locked="false" Priority="35" QFormat="true" Name="caption"/> | ||
| 93 | <w:LsdException Locked="false" Priority="10" SemiHidden="false" | ||
| 94 | UnhideWhenUsed="false" QFormat="true" Name="Title"/> | ||
| 95 | <w:LsdException Locked="false" Priority="1" Name="Default Paragraph Font"/> | ||
| 96 | <w:LsdException Locked="false" Priority="11" SemiHidden="false" | ||
| 97 | UnhideWhenUsed="false" QFormat="true" Name="Subtitle"/> | ||
| 98 | <w:LsdException Locked="false" Priority="22" SemiHidden="false" | ||
| 99 | UnhideWhenUsed="false" QFormat="true" Name="Strong"/> | ||
| 100 | <w:LsdException Locked="false" Priority="20" SemiHidden="false" | ||
| 101 | UnhideWhenUsed="false" QFormat="true" Name="Emphasis"/> | ||
| 102 | <w:LsdException Locked="false" Priority="59" SemiHidden="false" | ||
| 103 | UnhideWhenUsed="false" Name="Table Grid"/> | ||
| 104 | <w:LsdException Locked="false" UnhideWhenUsed="false" Name="Placeholder Text"/> | ||
| 105 | <w:LsdException Locked="false" Priority="1" SemiHidden="false" | ||
| 106 | UnhideWhenUsed="false" QFormat="true" Name="No Spacing"/> | ||
| 107 | <w:LsdException Locked="false" Priority="60" SemiHidden="false" | ||
| 108 | UnhideWhenUsed="false" Name="Light Shading"/> | ||
| 109 | <w:LsdException Locked="false" Priority="61" SemiHidden="false" | ||
| 110 | UnhideWhenUsed="false" Name="Light List"/> | ||
| 111 | <w:LsdException Locked="false" Priority="62" SemiHidden="false" | ||
| 112 | UnhideWhenUsed="false" Name="Light Grid"/> | ||
| 113 | <w:LsdException Locked="false" Priority="63" SemiHidden="false" | ||
| 114 | UnhideWhenUsed="false" Name="Medium Shading 1"/> | ||
| 115 | <w:LsdException Locked="false" Priority="64" SemiHidden="false" | ||
| 116 | UnhideWhenUsed="false" Name="Medium Shading 2"/> | ||
| 117 | <w:LsdException Locked="false" Priority="65" SemiHidden="false" | ||
| 118 | UnhideWhenUsed="false" Name="Medium List 1"/> | ||
| 119 | <w:LsdException Locked="false" Priority="66" SemiHidden="false" | ||
| 120 | UnhideWhenUsed="false" Name="Medium List 2"/> | ||
| 121 | <w:LsdException Locked="false" Priority="67" SemiHidden="false" | ||
| 122 | UnhideWhenUsed="false" Name="Medium Grid 1"/> | ||
| 123 | <w:LsdException Locked="false" Priority="68" SemiHidden="false" | ||
| 124 | UnhideWhenUsed="false" Name="Medium Grid 2"/> | ||
| 125 | <w:LsdException Locked="false" Priority="69" SemiHidden="false" | ||
| 126 | UnhideWhenUsed="false" Name="Medium Grid 3"/> | ||
| 127 | <w:LsdException Locked="false" Priority="70" SemiHidden="false" | ||
| 128 | UnhideWhenUsed="false" Name="Dark List"/> | ||
| 129 | <w:LsdException Locked="false" Priority="71" SemiHidden="false" | ||
| 130 | UnhideWhenUsed="false" Name="Colorful Shading"/> | ||
| 131 | <w:LsdException Locked="false" Priority="72" SemiHidden="false" | ||
| 132 | UnhideWhenUsed="false" Name="Colorful List"/> | ||
| 133 | <w:LsdException Locked="false" Priority="73" SemiHidden="false" | ||
| 134 | UnhideWhenUsed="false" Name="Colorful Grid"/> | ||
| 135 | <w:LsdException Locked="false" Priority="60" SemiHidden="false" | ||
| 136 | UnhideWhenUsed="false" Name="Light Shading Accent 1"/> | ||
| 137 | <w:LsdException Locked="false" Priority="61" SemiHidden="false" | ||
| 138 | UnhideWhenUsed="false" Name="Light List Accent 1"/> | ||
| 139 | <w:LsdException Locked="false" Priority="62" SemiHidden="false" | ||
| 140 | UnhideWhenUsed="false" Name="Light Grid Accent 1"/> | ||
| 141 | <w:LsdException Locked="false" Priority="63" SemiHidden="false" | ||
| 142 | UnhideWhenUsed="false" Name="Medium Shading 1 Accent 1"/> | ||
| 143 | <w:LsdException Locked="false" Priority="64" SemiHidden="false" | ||
| 144 | UnhideWhenUsed="false" Name="Medium Shading 2 Accent 1"/> | ||
| 145 | <w:LsdException Locked="false" Priority="65" SemiHidden="false" | ||
| 146 | UnhideWhenUsed="false" Name="Medium List 1 Accent 1"/> | ||
| 147 | <w:LsdException Locked="false" UnhideWhenUsed="false" Name="Revision"/> | ||
| 148 | <w:LsdException Locked="false" Priority="34" SemiHidden="false" | ||
| 149 | UnhideWhenUsed="false" QFormat="true" Name="List Paragraph"/> | ||
| 150 | <w:LsdException Locked="false" Priority="29" SemiHidden="false" | ||
| 151 | UnhideWhenUsed="false" QFormat="true" Name="Quote"/> | ||
| 152 | <w:LsdException Locked="false" Priority="30" SemiHidden="false" | ||
| 153 | UnhideWhenUsed="false" QFormat="true" Name="Intense Quote"/> | ||
| 154 | <w:LsdException Locked="false" Priority="66" SemiHidden="false" | ||
| 155 | UnhideWhenUsed="false" Name="Medium List 2 Accent 1"/> | ||
| 156 | <w:LsdException Locked="false" Priority="67" SemiHidden="false" | ||
| 157 | UnhideWhenUsed="false" Name="Medium Grid 1 Accent 1"/> | ||
| 158 | <w:LsdException Locked="false" Priority="68" SemiHidden="false" | ||
| 159 | UnhideWhenUsed="false" Name="Medium Grid 2 Accent 1"/> | ||
| 160 | <w:LsdException Locked="false" Priority="69" SemiHidden="false" | ||
| 161 | UnhideWhenUsed="false" Name="Medium Grid 3 Accent 1"/> | ||
| 162 | <w:LsdException Locked="false" Priority="70" SemiHidden="false" | ||
| 163 | UnhideWhenUsed="false" Name="Dark List Accent 1"/> | ||
| 164 | <w:LsdException Locked="false" Priority="71" SemiHidden="false" | ||
| 165 | UnhideWhenUsed="false" Name="Colorful Shading Accent 1"/> | ||
| 166 | <w:LsdException Locked="false" Priority="72" SemiHidden="false" | ||
| 167 | UnhideWhenUsed="false" Name="Colorful List Accent 1"/> | ||
| 168 | <w:LsdException Locked="false" Priority="73" SemiHidden="false" | ||
| 169 | UnhideWhenUsed="false" Name="Colorful Grid Accent 1"/> | ||
| 170 | <w:LsdException Locked="false" Priority="60" SemiHidden="false" | ||
| 171 | UnhideWhenUsed="false" Name="Light Shading Accent 2"/> | ||
| 172 | <w:LsdException Locked="false" Priority="61" SemiHidden="false" | ||
| 173 | UnhideWhenUsed="false" Name="Light List Accent 2"/> | ||
| 174 | <w:LsdException Locked="false" Priority="62" SemiHidden="false" | ||
| 175 | UnhideWhenUsed="false" Name="Light Grid Accent 2"/> | ||
| 176 | <w:LsdException Locked="false" Priority="63" SemiHidden="false" | ||
| 177 | UnhideWhenUsed="false" Name="Medium Shading 1 Accent 2"/> | ||
| 178 | <w:LsdException Locked="false" Priority="64" SemiHidden="false" | ||
| 179 | UnhideWhenUsed="false" Name="Medium Shading 2 Accent 2"/> | ||
| 180 | <w:LsdException Locked="false" Priority="65" SemiHidden="false" | ||
| 181 | UnhideWhenUsed="false" Name="Medium List 1 Accent 2"/> | ||
| 182 | <w:LsdException Locked="false" Priority="66" SemiHidden="false" | ||
| 183 | UnhideWhenUsed="false" Name="Medium List 2 Accent 2"/> | ||
| 184 | <w:LsdException Locked="false" Priority="67" SemiHidden="false" | ||
| 185 | UnhideWhenUsed="false" Name="Medium Grid 1 Accent 2"/> | ||
| 186 | <w:LsdException Locked="false" Priority="68" SemiHidden="false" | ||
| 187 | UnhideWhenUsed="false" Name="Medium Grid 2 Accent 2"/> | ||
| 188 | <w:LsdException Locked="false" Priority="69" SemiHidden="false" | ||
| 189 | UnhideWhenUsed="false" Name="Medium Grid 3 Accent 2"/> | ||
| 190 | <w:LsdException Locked="false" Priority="70" SemiHidden="false" | ||
| 191 | UnhideWhenUsed="false" Name="Dark List Accent 2"/> | ||
| 192 | <w:LsdException Locked="false" Priority="71" SemiHidden="false" | ||
| 193 | UnhideWhenUsed="false" Name="Colorful Shading Accent 2"/> | ||
| 194 | <w:LsdException Locked="false" Priority="72" SemiHidden="false" | ||
| 195 | UnhideWhenUsed="false" Name="Colorful List Accent 2"/> | ||
| 196 | <w:LsdException Locked="false" Priority="73" SemiHidden="false" | ||
| 197 | UnhideWhenUsed="false" Name="Colorful Grid Accent 2"/> | ||
| 198 | <w:LsdException Locked="false" Priority="60" SemiHidden="false" | ||
| 199 | UnhideWhenUsed="false" Name="Light Shading Accent 3"/> | ||
| 200 | <w:LsdException Locked="false" Priority="61" SemiHidden="false" | ||
| 201 | UnhideWhenUsed="false" Name="Light List Accent 3"/> | ||
| 202 | <w:LsdException Locked="false" Priority="62" SemiHidden="false" | ||
| 203 | UnhideWhenUsed="false" Name="Light Grid Accent 3"/> | ||
| 204 | <w:LsdException Locked="false" Priority="63" SemiHidden="false" | ||
| 205 | UnhideWhenUsed="false" Name="Medium Shading 1 Accent 3"/> | ||
| 206 | <w:LsdException Locked="false" Priority="64" SemiHidden="false" | ||
| 207 | UnhideWhenUsed="false" Name="Medium Shading 2 Accent 3"/> | ||
| 208 | <w:LsdException Locked="false" Priority="65" SemiHidden="false" | ||
| 209 | UnhideWhenUsed="false" Name="Medium List 1 Accent 3"/> | ||
| 210 | <w:LsdException Locked="false" Priority="66" SemiHidden="false" | ||
| 211 | UnhideWhenUsed="false" Name="Medium List 2 Accent 3"/> | ||
| 212 | <w:LsdException Locked="false" Priority="67" SemiHidden="false" | ||
| 213 | UnhideWhenUsed="false" Name="Medium Grid 1 Accent 3"/> | ||
| 214 | <w:LsdException Locked="false" Priority="68" SemiHidden="false" | ||
| 215 | UnhideWhenUsed="false" Name="Medium Grid 2 Accent 3"/> | ||
| 216 | <w:LsdException Locked="false" Priority="69" SemiHidden="false" | ||
| 217 | UnhideWhenUsed="false" Name="Medium Grid 3 Accent 3"/> | ||
| 218 | <w:LsdException Locked="false" Priority="70" SemiHidden="false" | ||
| 219 | UnhideWhenUsed="false" Name="Dark List Accent 3"/> | ||
| 220 | <w:LsdException Locked="false" Priority="71" SemiHidden="false" | ||
| 221 | UnhideWhenUsed="false" Name="Colorful Shading Accent 3"/> | ||
| 222 | <w:LsdException Locked="false" Priority="72" SemiHidden="false" | ||
| 223 | UnhideWhenUsed="false" Name="Colorful List Accent 3"/> | ||
| 224 | <w:LsdException Locked="false" Priority="73" SemiHidden="false" | ||
| 225 | UnhideWhenUsed="false" Name="Colorful Grid Accent 3"/> | ||
| 226 | <w:LsdException Locked="false" Priority="60" SemiHidden="false" | ||
| 227 | UnhideWhenUsed="false" Name="Light Shading Accent 4"/> | ||
| 228 | <w:LsdException Locked="false" Priority="61" SemiHidden="false" | ||
| 229 | UnhideWhenUsed="false" Name="Light List Accent 4"/> | ||
| 230 | <w:LsdException Locked="false" Priority="62" SemiHidden="false" | ||
| 231 | UnhideWhenUsed="false" Name="Light Grid Accent 4"/> | ||
| 232 | <w:LsdException Locked="false" Priority="63" SemiHidden="false" | ||
| 233 | UnhideWhenUsed="false" Name="Medium Shading 1 Accent 4"/> | ||
| 234 | <w:LsdException Locked="false" Priority="64" SemiHidden="false" | ||
| 235 | UnhideWhenUsed="false" Name="Medium Shading 2 Accent 4"/> | ||
| 236 | <w:LsdException Locked="false" Priority="65" SemiHidden="false" | ||
| 237 | UnhideWhenUsed="false" Name="Medium List 1 Accent 4"/> | ||
| 238 | <w:LsdException Locked="false" Priority="66" SemiHidden="false" | ||
| 239 | UnhideWhenUsed="false" Name="Medium List 2 Accent 4"/> | ||
| 240 | <w:LsdException Locked="false" Priority="67" SemiHidden="false" | ||
| 241 | UnhideWhenUsed="false" Name="Medium Grid 1 Accent 4"/> | ||
| 242 | <w:LsdException Locked="false" Priority="68" SemiHidden="false" | ||
| 243 | UnhideWhenUsed="false" Name="Medium Grid 2 Accent 4"/> | ||
| 244 | <w:LsdException Locked="false" Priority="69" SemiHidden="false" | ||
| 245 | UnhideWhenUsed="false" Name="Medium Grid 3 Accent 4"/> | ||
| 246 | <w:LsdException Locked="false" Priority="70" SemiHidden="false" | ||
| 247 | UnhideWhenUsed="false" Name="Dark List Accent 4"/> | ||
| 248 | <w:LsdException Locked="false" Priority="71" SemiHidden="false" | ||
| 249 | UnhideWhenUsed="false" Name="Colorful Shading Accent 4"/> | ||
| 250 | <w:LsdException Locked="false" Priority="72" SemiHidden="false" | ||
| 251 | UnhideWhenUsed="false" Name="Colorful List Accent 4"/> | ||
| 252 | <w:LsdException Locked="false" Priority="73" SemiHidden="false" | ||
| 253 | UnhideWhenUsed="false" Name="Colorful Grid Accent 4"/> | ||
| 254 | <w:LsdException Locked="false" Priority="60" SemiHidden="false" | ||
| 255 | UnhideWhenUsed="false" Name="Light Shading Accent 5"/> | ||
| 256 | <w:LsdException Locked="false" Priority="61" SemiHidden="false" | ||
| 257 | UnhideWhenUsed="false" Name="Light List Accent 5"/> | ||
| 258 | <w:LsdException Locked="false" Priority="62" SemiHidden="false" | ||
| 259 | UnhideWhenUsed="false" Name="Light Grid Accent 5"/> | ||
| 260 | <w:LsdException Locked="false" Priority="63" SemiHidden="false" | ||
| 261 | UnhideWhenUsed="false" Name="Medium Shading 1 Accent 5"/> | ||
| 262 | <w:LsdException Locked="false" Priority="64" SemiHidden="false" | ||
| 263 | UnhideWhenUsed="false" Name="Medium Shading 2 Accent 5"/> | ||
| 264 | <w:LsdException Locked="false" Priority="65" SemiHidden="false" | ||
| 265 | UnhideWhenUsed="false" Name="Medium List 1 Accent 5"/> | ||
| 266 | <w:LsdException Locked="false" Priority="66" SemiHidden="false" | ||
| 267 | UnhideWhenUsed="false" Name="Medium List 2 Accent 5"/> | ||
| 268 | <w:LsdException Locked="false" Priority="67" SemiHidden="false" | ||
| 269 | UnhideWhenUsed="false" Name="Medium Grid 1 Accent 5"/> | ||
| 270 | <w:LsdException Locked="false" Priority="68" SemiHidden="false" | ||
| 271 | UnhideWhenUsed="false" Name="Medium Grid 2 Accent 5"/> | ||
| 272 | <w:LsdException Locked="false" Priority="69" SemiHidden="false" | ||
| 273 | UnhideWhenUsed="false" Name="Medium Grid 3 Accent 5"/> | ||
| 274 | <w:LsdException Locked="false" Priority="70" SemiHidden="false" | ||
| 275 | UnhideWhenUsed="false" Name="Dark List Accent 5"/> | ||
| 276 | <w:LsdException Locked="false" Priority="71" SemiHidden="false" | ||
| 277 | UnhideWhenUsed="false" Name="Colorful Shading Accent 5"/> | ||
| 278 | <w:LsdException Locked="false" Priority="72" SemiHidden="false" | ||
| 279 | UnhideWhenUsed="false" Name="Colorful List Accent 5"/> | ||
| 280 | <w:LsdException Locked="false" Priority="73" SemiHidden="false" | ||
| 281 | UnhideWhenUsed="false" Name="Colorful Grid Accent 5"/> | ||
| 282 | <w:LsdException Locked="false" Priority="60" SemiHidden="false" | ||
| 283 | UnhideWhenUsed="false" Name="Light Shading Accent 6"/> | ||
| 284 | <w:LsdException Locked="false" Priority="61" SemiHidden="false" | ||
| 285 | UnhideWhenUsed="false" Name="Light List Accent 6"/> | ||
| 286 | <w:LsdException Locked="false" Priority="62" SemiHidden="false" | ||
| 287 | UnhideWhenUsed="false" Name="Light Grid Accent 6"/> | ||
| 288 | <w:LsdException Locked="false" Priority="63" SemiHidden="false" | ||
| 289 | UnhideWhenUsed="false" Name="Medium Shading 1 Accent 6"/> | ||
| 290 | <w:LsdException Locked="false" Priority="64" SemiHidden="false" | ||
| 291 | UnhideWhenUsed="false" Name="Medium Shading 2 Accent 6"/> | ||
| 292 | <w:LsdException Locked="false" Priority="65" SemiHidden="false" | ||
| 293 | UnhideWhenUsed="false" Name="Medium List 1 Accent 6"/> | ||
| 294 | <w:LsdException Locked="false" Priority="66" SemiHidden="false" | ||
| 295 | UnhideWhenUsed="false" Name="Medium List 2 Accent 6"/> | ||
| 296 | <w:LsdException Locked="false" Priority="67" SemiHidden="false" | ||
| 297 | UnhideWhenUsed="false" Name="Medium Grid 1 Accent 6"/> | ||
| 298 | <w:LsdException Locked="false" Priority="68" SemiHidden="false" | ||
| 299 | UnhideWhenUsed="false" Name="Medium Grid 2 Accent 6"/> | ||
| 300 | <w:LsdException Locked="false" Priority="69" SemiHidden="false" | ||
| 301 | UnhideWhenUsed="false" Name="Medium Grid 3 Accent 6"/> | ||
| 302 | <w:LsdException Locked="false" Priority="70" SemiHidden="false" | ||
| 303 | UnhideWhenUsed="false" Name="Dark List Accent 6"/> | ||
| 304 | <w:LsdException Locked="false" Priority="71" SemiHidden="false" | ||
| 305 | UnhideWhenUsed="false" Name="Colorful Shading Accent 6"/> | ||
| 306 | <w:LsdException Locked="false" Priority="72" SemiHidden="false" | ||
| 307 | UnhideWhenUsed="false" Name="Colorful List Accent 6"/> | ||
| 308 | <w:LsdException Locked="false" Priority="73" SemiHidden="false" | ||
| 309 | UnhideWhenUsed="false" Name="Colorful Grid Accent 6"/> | ||
| 310 | <w:LsdException Locked="false" Priority="19" SemiHidden="false" | ||
| 311 | UnhideWhenUsed="false" QFormat="true" Name="Subtle Emphasis"/> | ||
| 312 | <w:LsdException Locked="false" Priority="21" SemiHidden="false" | ||
| 313 | UnhideWhenUsed="false" QFormat="true" Name="Intense Emphasis"/> | ||
| 314 | <w:LsdException Locked="false" Priority="31" SemiHidden="false" | ||
| 315 | UnhideWhenUsed="false" QFormat="true" Name="Subtle Reference"/> | ||
| 316 | <w:LsdException Locked="false" Priority="32" SemiHidden="false" | ||
| 317 | UnhideWhenUsed="false" QFormat="true" Name="Intense Reference"/> | ||
| 318 | <w:LsdException Locked="false" Priority="33" SemiHidden="false" | ||
| 319 | UnhideWhenUsed="false" QFormat="true" Name="Book Title"/> | ||
| 320 | <w:LsdException Locked="false" Priority="37" Name="Bibliography"/> | ||
| 321 | <w:LsdException Locked="false" Priority="39" QFormat="true" Name="TOC Heading"/> | ||
| 322 | </w:LatentStyles> | ||
| 323 | </xml><![endif]--> | ||
| 324 | <style> | ||
| 325 | <!-- | ||
| 326 | /* Font Definitions */ | ||
| 327 | @font-face | ||
| 328 | 	{font-family:Wingdings; | ||
| 329 | 	panose-1:5 0 0 0 0 0 0 0 0 0; | ||
| 330 | 	mso-font-charset:2; | ||
| 331 | 	mso-generic-font-family:auto; | ||
| 332 | 	mso-font-pitch:variable; | ||
| 333 | 	mso-font-signature:0 268435456 0 0 -2147483648 0;} | ||
| 334 | @font-face | ||
| 335 | 	{font-family:Wingdings; | ||
| 336 | 	panose-1:5 0 0 0 0 0 0 0 0 0; | ||
| 337 | 	mso-font-charset:2; | ||
| 338 | 	mso-generic-font-family:auto; | ||
| 339 | 	mso-font-pitch:variable; | ||
| 340 | 	mso-font-signature:0 268435456 0 0 -2147483648 0;} | ||
| 341 | @font-face | ||
| 342 | 	{font-family:Calibri; | ||
| 343 | 	panose-1:2 15 5 2 2 2 4 3 2 4; | ||
| 344 | 	mso-font-charset:0; | ||
| 345 | 	mso-generic-font-family:swiss; | ||
| 346 | 	mso-font-pitch:variable; | ||
| 347 | 	mso-font-signature:-520092929 1073786111 9 0 415 0;} | ||
| 348 | /* Style Definitions */ | ||
| 349 | p.MsoNormal, li.MsoNormal, div.MsoNormal | ||
| 350 | 	{mso-style-unhide:no; | ||
| 351 | 	mso-style-qformat:yes; | ||
| 352 | 	mso-style-parent:""; | ||
| 353 | 	margin-top:0in; | ||
| 354 | 	margin-right:0in; | ||
| 355 | 	margin-bottom:10.0pt; | ||
| 356 | 	margin-left:0in; | ||
| 357 | 	line-height:115%; | ||
| 358 | 	mso-pagination:widow-orphan; | ||
| 359 | 	font-size:11.0pt; | ||
| 360 | 	font-family:"Calibri","sans-serif"; | ||
| 361 | 	mso-ascii-font-family:Calibri; | ||
| 362 | 	mso-ascii-theme-font:minor-latin; | ||
| 363 | 	mso-fareast-font-family:Calibri; | ||
| 364 | 	mso-fareast-theme-font:minor-latin; | ||
| 365 | 	mso-hansi-font-family:Calibri; | ||
| 366 | 	mso-hansi-theme-font:minor-latin; | ||
| 367 | 	mso-bidi-font-family:"Times New Roman"; | ||
| 368 | 	mso-bidi-theme-font:minor-bidi;} | ||
| 369 | p.MsoListParagraph, li.MsoListParagraph, div.MsoListParagraph | ||
| 370 | 	{mso-style-priority:34; | ||
| 371 | 	mso-style-unhide:no; | ||
| 372 | 	mso-style-qformat:yes; | ||
| 373 | 	margin-top:0in; | ||
| 374 | 	margin-right:0in; | ||
| 375 | 	margin-bottom:10.0pt; | ||
| 376 | 	margin-left:.5in; | ||
| 377 | 	mso-add-space:auto; | ||
| 378 | 	line-height:115%; | ||
| 379 | 	mso-pagination:widow-orphan; | ||
| 380 | 	font-size:11.0pt; | ||
| 381 | 	font-family:"Calibri","sans-serif"; | ||
| 382 | 	mso-ascii-font-family:Calibri; | ||
| 383 | 	mso-ascii-theme-font:minor-latin; | ||
| 384 | 	mso-fareast-font-family:Calibri; | ||
| 385 | 	mso-fareast-theme-font:minor-latin; | ||
| 386 | 	mso-hansi-font-family:Calibri; | ||
| 387 | 	mso-hansi-theme-font:minor-latin; | ||
| 388 | 	mso-bidi-font-family:"Times New Roman"; | ||
| 389 | 	mso-bidi-theme-font:minor-bidi;} | ||
| 390 | p.MsoListParagraphCxSpFirst, li.MsoListParagraphCxSpFirst, div.MsoListParagraphCxSpFirst | ||
| 391 | 	{mso-style-priority:34; | ||
| 392 | 	mso-style-unhide:no; | ||
| 393 | 	mso-style-qformat:yes; | ||
| 394 | 	mso-style-type:export-only; | ||
| 395 | 	margin-top:0in; | ||
| 396 | 	margin-right:0in; | ||
| 397 | 	margin-bottom:0in; | ||
| 398 | 	margin-left:.5in; | ||
| 399 | 	margin-bottom:.0001pt; | ||
| 400 | 	mso-add-space:auto; | ||
| 401 | 	line-height:115%; | ||
| 402 | 	mso-pagination:widow-orphan; | ||
| 403 | 	font-size:11.0pt; | ||
| 404 | 	font-family:"Calibri","sans-serif"; | ||
| 405 | 	mso-ascii-font-family:Calibri; | ||
| 406 | 	mso-ascii-theme-font:minor-latin; | ||
| 407 | 	mso-fareast-font-family:Calibri; | ||
| 408 | 	mso-fareast-theme-font:minor-latin; | ||
| 409 | 	mso-hansi-font-family:Calibri; | ||
| 410 | 	mso-hansi-theme-font:minor-latin; | ||
| 411 | 	mso-bidi-font-family:"Times New Roman"; | ||
| 412 | 	mso-bidi-theme-font:minor-bidi;} | ||
| 413 | p.MsoListParagraphCxSpMiddle, li.MsoListParagraphCxSpMiddle, div.MsoListParagraphCxSpMiddle | ||
| 414 | 	{mso-style-priority:34; | ||
| 415 | 	mso-style-unhide:no; | ||
| 416 | 	mso-style-qformat:yes; | ||
| 417 | 	mso-style-type:export-only; | ||
| 418 | 	margin-top:0in; | ||
| 419 | 	margin-right:0in; | ||
| 420 | 	margin-bottom:0in; | ||
| 421 | 	margin-left:.5in; | ||
| 422 | 	margin-bottom:.0001pt; | ||
| 423 | 	mso-add-space:auto; | ||
| 424 | 	line-height:115%; | ||
| 425 | 	mso-pagination:widow-orphan; | ||
| 426 | 	font-size:11.0pt; | ||
| 427 | 	font-family:"Calibri","sans-serif"; | ||
| 428 | 	mso-ascii-font-family:Calibri; | ||
| 429 | 	mso-ascii-theme-font:minor-latin; | ||
| 430 | 	mso-fareast-font-family:Calibri; | ||
| 431 | 	mso-fareast-theme-font:minor-latin; | ||
| 432 | 	mso-hansi-font-family:Calibri; | ||
| 433 | 	mso-hansi-theme-font:minor-latin; | ||
| 434 | 	mso-bidi-font-family:"Times New Roman"; | ||
| 435 | 	mso-bidi-theme-font:minor-bidi;} | ||
| 436 | p.MsoListParagraphCxSpLast, li.MsoListParagraphCxSpLast, div.MsoListParagraphCxSpLast | ||
| 437 | 	{mso-style-priority:34; | ||
| 438 | 	mso-style-unhide:no; | ||
| 439 | 	mso-style-qformat:yes; | ||
| 440 | 	mso-style-type:export-only; | ||
| 441 | 	margin-top:0in; | ||
| 442 | 	margin-right:0in; | ||
| 443 | 	margin-bottom:10.0pt; | ||
| 444 | 	margin-left:.5in; | ||
| 445 | 	mso-add-space:auto; | ||
| 446 | 	line-height:115%; | ||
| 447 | 	mso-pagination:widow-orphan; | ||
| 448 | 	font-size:11.0pt; | ||
| 449 | 	font-family:"Calibri","sans-serif"; | ||
| 450 | 	mso-ascii-font-family:Calibri; | ||
| 451 | 	mso-ascii-theme-font:minor-latin; | ||
| 452 | 	mso-fareast-font-family:Calibri; | ||
| 453 | 	mso-fareast-theme-font:minor-latin; | ||
| 454 | 	mso-hansi-font-family:Calibri; | ||
| 455 | 	mso-hansi-theme-font:minor-latin; | ||
| 456 | 	mso-bidi-font-family:"Times New Roman"; | ||
| 457 | 	mso-bidi-theme-font:minor-bidi;} | ||
| 458 | .MsoChpDefault | ||
| 459 | 	{mso-style-type:export-only; | ||
| 460 | 	mso-default-props:yes; | ||
| 461 | 	font-family:"Calibri","sans-serif"; | ||
| 462 | 	mso-ascii-font-family:Calibri; | ||
| 463 | 	mso-ascii-theme-font:minor-latin; | ||
| 464 | 	mso-fareast-font-family:Calibri; | ||
| 465 | 	mso-fareast-theme-font:minor-latin; | ||
| 466 | 	mso-hansi-font-family:Calibri; | ||
| 467 | 	mso-hansi-theme-font:minor-latin; | ||
| 468 | 	mso-bidi-font-family:"Times New Roman"; | ||
| 469 | 	mso-bidi-theme-font:minor-bidi;} | ||
| 470 | .MsoPapDefault | ||
| 471 | 	{mso-style-type:export-only; | ||
| 472 | 	margin-bottom:10.0pt; | ||
| 473 | 	line-height:115%;} | ||
| 474 | @page WordSection1 | ||
| 475 | 	{size:8.5in 11.0in; | ||
| 476 | 	margin:1.0in 1.0in 1.0in 1.0in; | ||
| 477 | 	mso-header-margin:.5in; | ||
| 478 | 	mso-footer-margin:.5in; | ||
| 479 | 	mso-paper-source:0;} | ||
| 480 | div.WordSection1 | ||
| 481 | 	{page:WordSection1;} | ||
| 482 | /* List Definitions */ | ||
| 483 | @list l0 | ||
| 484 | 	{mso-list-id:727800390; | ||
| 485 | 	mso-list-type:hybrid; | ||
| 486 | 	mso-list-template-ids:-60157512 67698703 67698713 67698715 67698703 67698713 67698715 67698703 67698713 67698715;} | ||
| 487 | @list l0:level1 | ||
| 488 | 	{mso-level-tab-stop:none; | ||
| 489 | 	mso-level-number-position:left; | ||
| 490 | 	text-indent:-.25in;} | ||
| 491 | @list l0:level2 | ||
| 492 | 	{mso-level-number-format:alpha-lower; | ||
| 493 | 	mso-level-tab-stop:none; | ||
| 494 | 	mso-level-number-position:left; | ||
| 495 | 	text-indent:-.25in;} | ||
| 496 | @list l0:level3 | ||
| 497 | 	{mso-level-number-format:roman-lower; | ||
| 498 | 	mso-level-tab-stop:none; | ||
| 499 | 	mso-level-number-position:right; | ||
| 500 | 	text-indent:-9.0pt;} | ||
| 501 | @list l0:level4 | ||
| 502 | 	{mso-level-tab-stop:none; | ||
| 503 | 	mso-level-number-position:left; | ||
| 504 | 	text-indent:-.25in;} | ||
| 505 | @list l0:level5 | ||
| 506 | 	{mso-level-number-format:alpha-lower; | ||
| 507 | 	mso-level-tab-stop:none; | ||
| 508 | 	mso-level-number-position:left; | ||
| 509 | 	text-indent:-.25in;} | ||
| 510 | @list l0:level6 | ||
| 511 | 	{mso-level-number-format:roman-lower; | ||
| 512 | 	mso-level-tab-stop:none; | ||
| 513 | 	mso-level-number-position:right; | ||
| 514 | 	text-indent:-9.0pt;} | ||
| 515 | @list l0:level7 | ||
| 516 | 	{mso-level-tab-stop:none; | ||
| 517 | 	mso-level-number-position:left; | ||
| 518 | 	text-indent:-.25in;} | ||
| 519 | @list l0:level8 | ||
| 520 | 	{mso-level-number-format:alpha-lower; | ||
| 521 | 	mso-level-tab-stop:none; | ||
| 522 | 	mso-level-number-position:left; | ||
| 523 | 	text-indent:-.25in;} | ||
| 524 | @list l0:level9 | ||
| 525 | 	{mso-level-number-format:roman-lower; | ||
| 526 | 	mso-level-tab-stop:none; | ||
| 527 | 	mso-level-number-position:right; | ||
| 528 | 	text-indent:-9.0pt;} | ||
| 529 | @list l1 | ||
| 530 | 	{mso-list-id:1613515428; | ||
| 531 | 	mso-list-type:hybrid; | ||
| 532 | 	mso-list-template-ids:-1154053724 -532929662 67698691 67698693 67698689 67698691 67698693 67698689 67698691 67698693;} | ||
| 533 | @list l1:level1 | ||
| 534 | 	{mso-level-start-at:0; | ||
| 535 | 	mso-level-number-format:bullet; | ||
| 536 | 	mso-level-text:ï‚·; | ||
| 537 | 	mso-level-tab-stop:none; | ||
| 538 | 	mso-level-number-position:left; | ||
| 539 | 	text-indent:-.25in; | ||
| 540 | 	font-family:Symbol; | ||
| 541 | 	mso-fareast-font-family:Calibri; | ||
| 542 | 	mso-fareast-theme-font:minor-latin; | ||
| 543 | 	mso-bidi-font-family:"Times New Roman"; | ||
| 544 | 	mso-bidi-theme-font:minor-bidi;} | ||
| 545 | @list l1:level2 | ||
| 546 | 	{mso-level-number-format:bullet; | ||
| 547 | 	mso-level-text:o; | ||
| 548 | 	mso-level-tab-stop:none; | ||
| 549 | 	mso-level-number-position:left; | ||
| 550 | 	text-indent:-.25in; | ||
| 551 | 	font-family:"Courier New";} | ||
| 552 | @list l1:level3 | ||
| 553 | 	{mso-level-number-format:bullet; | ||
| 554 | 	mso-level-text:ï‚§; | ||
| 555 | 	mso-level-tab-stop:none; | ||
| 556 | 	mso-level-number-position:left; | ||
| 557 | 	text-indent:-.25in; | ||
| 558 | 	font-family:Wingdings;} | ||
| 559 | @list l1:level4 | ||
| 560 | 	{mso-level-number-format:bullet; | ||
| 561 | 	mso-level-text:ï‚·; | ||
| 562 | 	mso-level-tab-stop:none; | ||
| 563 | 	mso-level-number-position:left; | ||
| 564 | 	text-indent:-.25in; | ||
| 565 | 	font-family:Symbol;} | ||
| 566 | @list l1:level5 | ||
| 567 | 	{mso-level-number-format:bullet; | ||
| 568 | 	mso-level-text:o; | ||
| 569 | 	mso-level-tab-stop:none; | ||
| 570 | 	mso-level-number-position:left; | ||
| 571 | 	text-indent:-.25in; | ||
| 572 | 	font-family:"Courier New";} | ||
| 573 | @list l1:level6 | ||
| 574 | 	{mso-level-number-format:bullet; | ||
| 575 | 	mso-level-text:ï‚§; | ||
| 576 | 	mso-level-tab-stop:none; | ||
| 577 | 	mso-level-number-position:left; | ||
| 578 | 	text-indent:-.25in; | ||
| 579 | 	font-family:Wingdings;} | ||
| 580 | @list l1:level7 | ||
| 581 | 	{mso-level-number-format:bullet; | ||
| 582 | 	mso-level-text:ï‚·; | ||
| 583 | 	mso-level-tab-stop:none; | ||
| 584 | 	mso-level-number-position:left; | ||
| 585 | 	text-indent:-.25in; | ||
| 586 | 	font-family:Symbol;} | ||
| 587 | @list l1:level8 | ||
| 588 | 	{mso-level-number-format:bullet; | ||
| 589 | 	mso-level-text:o; | ||
| 590 | 	mso-level-tab-stop:none; | ||
| 591 | 	mso-level-number-position:left; | ||
| 592 | 	text-indent:-.25in; | ||
| 593 | 	font-family:"Courier New";} | ||
| 594 | @list l1:level9 | ||
| 595 | 	{mso-level-number-format:bullet; | ||
| 596 | 	mso-level-text:ï‚§; | ||
| 597 | 	mso-level-tab-stop:none; | ||
| 598 | 	mso-level-number-position:left; | ||
| 599 | 	text-indent:-.25in; | ||
| 600 | 	font-family:Wingdings;} | ||
| 601 | ol | ||
| 602 | 	{margin-bottom:0in;} | ||
| 603 | ul | ||
| 604 | 	{margin-bottom:0in;} | ||
| 605 | --> | ||
| 606 | </style> | ||
| 607 | <!--[if gte mso 10]> | ||
| 608 | <style> | ||
| 609 | /* Style Definitions */ | ||
| 610 | table.MsoNormalTable | ||
| 611 | 	{mso-style-name:"Table Normal"; | ||
| 612 | 	mso-tstyle-rowband-size:0; | ||
| 613 | 	mso-tstyle-colband-size:0; | ||
| 614 | 	mso-style-noshow:yes; | ||
| 615 | 	mso-style-priority:99; | ||
| 616 | 	mso-style-parent:""; | ||
| 617 | 	mso-padding-alt:0in 5.4pt 0in 5.4pt; | ||
| 618 | 	mso-para-margin-top:0in; | ||
| 619 | 	mso-para-margin-right:0in; | ||
| 620 | 	mso-para-margin-bottom:10.0pt; | ||
| 621 | 	mso-para-margin-left:0in; | ||
| 622 | 	line-height:115%; | ||
| 623 | 	mso-pagination:widow-orphan; | ||
| 624 | 	font-size:11.0pt; | ||
| 625 | 	font-family:"Calibri","sans-serif"; | ||
| 626 | 	mso-ascii-font-family:Calibri; | ||
| 627 | 	mso-ascii-theme-font:minor-latin; | ||
| 628 | 	mso-hansi-font-family:Calibri; | ||
| 629 | 	mso-hansi-theme-font:minor-latin; | ||
| 630 | 	mso-bidi-font-family:"Times New Roman"; | ||
| 631 | 	mso-bidi-theme-font:minor-bidi;} | ||
| 632 | </style> | ||
| 633 | <![endif]--> | ||
| 634 | </head> | ||
| 635 | |||
| 636 | <body lang=EN-US style='tab-interval:.5in'> | ||
| 637 | <!--StartFragment--> | ||
| 638 | |||
| 639 | <p class=MsoListParagraphCxSpFirst style='text-indent:-.25in;mso-list:l1 level1 lfo1'><![if !supportLists]><span | ||
| 640 | style='font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family: | ||
| 641 | Symbol'><span style='mso-list:Ignore'>·<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | ||
| 642 | </span></span></span><![endif]>Bullet one<o:p></o:p></p> | ||
| 643 | |||
| 644 | <p class=MsoListParagraphCxSpMiddle style='margin-left:1.0in;mso-add-space: | ||
| 645 | auto;text-indent:-.25in;mso-list:l1 level2 lfo1'><![if !supportLists]><span | ||
| 646 | style='font-family:"Courier New";mso-fareast-font-family:"Courier New"'><span | ||
| 647 | style='mso-list:Ignore'>o<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp; | ||
| 648 | </span></span></span><![endif]>Bullet nested<o:p></o:p></p> | ||
| 649 | |||
| 650 | <p class=MsoListParagraphCxSpMiddle style='margin-left:1.5in;mso-add-space: | ||
| 651 | auto;text-indent:-.25in;mso-list:l1 level3 lfo1'><![if !supportLists]><span | ||
| 652 | style='font-family:Wingdings;mso-fareast-font-family:Wingdings;mso-bidi-font-family: | ||
| 653 | Wingdings'><span style='mso-list:Ignore'>§<span style='font:7.0pt "Times New Roman"'>&nbsp; | ||
| 654 | </span></span></span><![endif]>Bullet deeper<o:p></o:p></p> | ||
| 655 | |||
| 656 | <p class=MsoListParagraphCxSpLast style='text-indent:-.25in;mso-list:l1 level1 lfo1'><![if !supportLists]><span | ||
| 657 | style='font-family:Symbol;mso-fareast-font-family:Symbol;mso-bidi-font-family: | ||
| 658 | Symbol'><span style='mso-list:Ignore'>·<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | ||
| 659 | </span></span></span><![endif]>Bullet two<o:p></o:p></p> | ||
| 660 | |||
| 661 | <p class=MsoNormal>Plain paragraph<o:p></o:p></p> | ||
| 662 | |||
| 663 | <p class=MsoListParagraphCxSpFirst style='text-indent:-.25in;mso-list:l0 level1 lfo2'><![if !supportLists]><span | ||
| 664 | style='mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin'><span | ||
| 665 | style='mso-list:Ignore'>1.<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | ||
| 666 | </span></span></span><![endif]>Number one<o:p></o:p></p> | ||
| 667 | |||
| 668 | <p class=MsoListParagraphCxSpMiddle style='margin-left:1.0in;mso-add-space: | ||
| 669 | auto;text-indent:-.25in;mso-list:l0 level2 lfo2'><![if !supportLists]><span | ||
| 670 | style='mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin'><span | ||
| 671 | style='mso-list:Ignore'>a.<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | ||
| 672 | </span></span></span><![endif]>Number nested<o:p></o:p></p> | ||
| 673 | |||
| 674 | <p class=MsoListParagraphCxSpMiddle style='margin-left:1.5in;mso-add-space: | ||
| 675 | auto;text-indent:-1.5in;mso-text-indent-alt:-9.0pt;mso-list:l0 level3 lfo2'><![if !supportLists]><span | ||
| 676 | style='mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin'><span | ||
| 677 | style='mso-list:Ignore'><span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | ||
| 678 | </span>i.<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | ||
| 679 | </span></span></span><![endif]>Number deeper<o:p></o:p></p> | ||
| 680 | |||
| 681 | <p class=MsoListParagraphCxSpLast style='text-indent:-.25in;mso-list:l0 level1 lfo2'><![if !supportLists]><span | ||
| 682 | style='mso-bidi-font-family:Calibri;mso-bidi-theme-font:minor-latin'><span | ||
| 683 | style='mso-list:Ignore'>2.<span style='font:7.0pt "Times New Roman"'>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | ||
| 684 | </span></span></span><![endif]>Number two<o:p></o:p></p> | ||
| 685 | |||
| 686 | <p class=MsoNormal>Last plain<o:p></o:p></p> | ||
| 687 | |||
| 688 | <!--EndFragment--> | ||
| 689 | </body> | ||
| 690 | |||
| 691 | </html> | ||
corpus/word-lists/README.md created+12| ... | @@ -0,0 +1,12 @@ | ||
| 1 | # Lists pasted from Word | ||
| 2 | |||
| 3 | `candidate/` is `crates/canvas/tests/clip_paste.rs`'s `words_lists_paste_and_store` | ||
| 4 | section: Word 2010's copy of three-level bulleted and numbered lists | ||
| 5 | (`corpus/clipboard/word-2010.html`) pasted through the editor at a caret on blank page, | ||
| 6 | stored as one edit. Bullets keep Word's glyph and font with no gallery index | ||
| 7 | (`ListMSAAIndex`), as OneNote 2010 stored the same paste (lab, 2026-10-02). | ||
| 8 | `cold/` is a fresh OneNote 2010 read: the bullets read back as the ones OneNote's own | ||
| 9 | paste gave (`bullet` 1, 0 and 13), and the numbers as 1., a., i. and 2. | ||
| 10 | `tools/test_word_lists.py` checks the capture without a VM. Regenerate with | ||
| 11 | `CANVAS_WORD_LIST_EXPORT` set to a directory while running the test, then | ||
| 12 | `tools/native_runner.py DIR COLD --expected-pages 1 --collect-notebook --screenshots`. | ||
corpus/word-lists/candidate/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/word-lists/candidate/Open Notebook.onetoc2 differ | |||
corpus/word-lists/candidate/word.one created| Binary files /dev/null and b/corpus/word-lists/candidate/word.one differ | |||
corpus/word-lists/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-237B5BEB -ExpectedPages 1 -KeepOpen", "exit": 0, "stdout": "Read 1 sections and 1 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/word-lists/cold/machine.json created+1| ... | @@ -0,0 +1 @@ | ||
| 1 | {"name": "m6-237b5beb", "hostname": "ONE-M6-237B5BEB"} | ||
corpus/word-lists/cold/notebook/Open Notebook.onetoc2 created| Binary files /dev/null and b/corpus/word-lists/cold/notebook/Open Notebook.onetoc2 differ | |||
corpus/word-lists/cold/notebook/word.one created| Binary files /dev/null and b/corpus/word-lists/cold/notebook/word.one differ | |||
corpus/word-lists/cold/read/environment.json created+7| ... | @@ -0,0 +1,7 @@ | ||
| 1 | { | ||
| 2 | "powershell": "5.1.14409.1005", | ||
| 3 | "schema": "xs2010", | ||
| 4 | "hostname": "ONE-M6-237B5BEB", | ||
| 5 | "cold": true, | ||
| 6 | "onenote": "14.0.4763.1000" | ||
| 7 | } | ||
corpus/word-lists/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="{2E97477E-5494-424D-A410-E4D8E87E2C2F}{1}{B0}" path="C:\one-tests\runs\capture\notebook" lastModifiedTime="2026-10-02T19:03:33.000Z" color="#B49EDE"><one:Section name="word" ID="{E6BEBA52-50C8-0D11-01D6-CD4A9683CA34}{1}{B0}" path="C:\one-tests\runs\capture\notebook\word.one" lastModifiedTime="2026-10-02T19:03:33.000Z" color="#8AA8E4"><one:Page ID="{8DF8F58E-417D-0774-19CC-80F9239344E5}{1}{B0}" name="Word lists" dateTime="2026-10-02T18:59:39.000Z" lastModifiedTime="2026-10-02T19:03:33.000Z" pageLevel="1"/></one:Section></one:Notebook> | ||
corpus/word-lists/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": "Word lists - Microsoft OneNote", | ||
| 10 | "class": "Framework::CFrame", | ||
| 11 | "dialog": false | ||
| 12 | } | ||
| 13 | } | ||
| \ No newline at end of file | |||
corpus/word-lists/cold/read/page-000.png created| Binary files /dev/null and b/corpus/word-lists/cold/read/page-000.png differ | |||
corpus/word-lists/cold/read/page-000.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="{8DF8F58E-417D-0774-19CC-80F9239344E5}{1}{B0}" name="Word lists" dateTime="2026-10-02T18:59:39.000Z" lastModifiedTime="2026-10-02T19:03:33.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" authorInitials="A" lastModifiedBy="Author" lastModifiedByInitials="A" lastModifiedTime="2026-10-02T18:59:39.000Z" objectID="{E6BEBA52-50C8-0D11-01D6-CD4A9683CA34}{23}{B0}"><one:Position x="36.0" y="36.0" z="0"/><one:Size width="84.77598571777344" height="13.4277172088623"/><one:OEChildren><one:OE creationTime="2026-10-02T18:59:39.000Z" lastModifiedTime="2026-10-02T18:59:39.000Z" objectID="{E6BEBA52-50C8-0D11-01D6-CD4A9683CA34}{24}{B0}" alignment="left"><one:T><![CDATA[Word lists]]></one:T></one:OE></one:OEChildren></one:Outline><one:Outline author="Author" authorInitials="A" lastModifiedBy="Author" lastModifiedByInitials="A" lastModifiedTime="2022-06-18T04:26:40.000Z" objectID="{EB92038B-8B15-0AED-0E44-D334EF652A6D}{122}{B0}"><one:Position x="72.0" y="120.0" z="1"/><one:Size width="164.8041687011719" height="135.7771759033203"/><one:OEChildren><one:OE creationTime="2022-06-18T04:26:40.000Z" lastModifiedTime="2022-06-18T04:26:40.000Z" objectID="{EB92038B-8B15-0AED-0E44-D334EF652A6D}{159}{B0}" alignment="left"><one:List><one:Bullet bullet="1" fontSize="11.0"/></one:List><one:T><![CDATA[Bullet one]]></one:T><one:OEChildren><one:OE creationTime="2022-06-18T04:26:40.000Z" lastModifiedTime="2022-06-18T04:26:40.000Z" objectID="{EB92038B-8B15-0AED-0E44-D334EF652A6D}{161}{B0}" alignment="left"><one:List><one:Bullet bullet="0" fontSize="11.0"/></one:List><one:T><![CDATA[Bullet nested]]></one:T><one:OEChildren><one:OE creationTime="2022-06-18T04:26:40.000Z" lastModifiedTime="2022-06-18T04:26:40.000Z" objectID="{EB92038B-8B15-0AED-0E44-D334EF652A6D}{163}{B0}" alignment="left"><one:List><one:Bullet bullet="13" fontSize="11.0"/></one:List><one:T><![CDATA[Bullet deeper]]></one:T></one:OE></one:OEChildren></one:OE></one:OEChildren></one:OE><one:OE creationTime="2022-06-18T04:26:40.000Z" lastModifiedTime="2022-06-18T04:26:40.000Z" objectID="{EB92038B-8B15-0AED-0E44-D334EF652A6D}{165}{B0}" alignment="left"><one:List><one:Bullet bullet="1" fontSize="11.0"/></one:List><one:T><![CDATA[Bullet two]]></one:T></one:OE><one:OE creationTime="2022-06-18T04:26:40.000Z" lastModifiedTime="2022-06-18T04:26:40.000Z" objectID="{EB92038B-8B15-0AED-0E44-D334EF652A6D}{167}{B0}" alignment="left"><one:T><![CDATA[Plain paragraph]]></one:T></one:OE><one:OE creationTime="2022-06-18T04:26:40.000Z" lastModifiedTime="2022-06-18T04:26:40.000Z" objectID="{EB92038B-8B15-0AED-0E44-D334EF652A6D}{169}{B0}" alignment="left"><one:List><one:Number numberSequence="0" numberFormat="##." fontSize="11.0" font="Calibri" text="1."/></one:List><one:T><![CDATA[Number one]]></one:T><one:OEChildren><one:OE creationTime="2022-06-18T04:26:40.000Z" lastModifiedTime="2022-06-18T04:26:40.000Z" objectID="{EB92038B-8B15-0AED-0E44-D334EF652A6D}{171}{B0}" alignment="left"><one:List><one:Number numberSequence="4" numberFormat="##." fontSize="11.0" font="Calibri" text="a."/></one:List><one:T><![CDATA[Number nested]]></one:T><one:OEChildren><one:OE creationTime="2022-06-18T04:26:40.000Z" lastModifiedTime="2022-06-18T04:26:40.000Z" objectID="{EB92038B-8B15-0AED-0E44-D334EF652A6D}{173}{B0}" alignment="left"><one:List><one:Number numberSequence="2" numberFormat="##." fontSize="11.0" font="Calibri" text="i."/></one:List><one:T><![CDATA[Number deeper]]></one:T></one:OE></one:OEChildren></one:OE></one:OEChildren></one:OE><one:OE creationTime="2022-06-18T04:26:40.000Z" lastModifiedTime="2022-06-18T04:26:40.000Z" objectID="{EB92038B-8B15-0AED-0E44-D334EF652A6D}{175}{B0}" alignment="left"><one:List><one:Number numberSequence="0" numberFormat="##." fontSize="11.0" font="Calibri" text="2."/></one:List><one:T><![CDATA[Number two]]></one:T></one:OE><one:OE creationTime="2022-06-18T04:26:40.000Z" lastModifiedTime="2022-06-18T04:26:40.000Z" objectID="{EB92038B-8B15-0AED-0E44-D334EF652A6D}{177}{B0}" alignment="left"><one:T><![CDATA[Last plain]]></one:T></one:OE></one:OEChildren></one:Outline></one:Page> | ||
corpus/word-lists/cold/read/payloads.json created+3| ... | @@ -0,0 +1,3 @@ | ||
| 1 | [ | ||
| 2 | |||
| 3 | ] | ||
| \ No newline at end of file | |||
corpus/word-lists/cold/run.json created+18| ... | @@ -0,0 +1,18 @@ | ||
| 1 | { | ||
| 2 | "notebook": "/private/tmp/pastefix/cand61", | ||
| 3 | "expected_pages": 1, | ||
| 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": "22bc760f955ef59bb5f0ccfd55271f8d868674084127fbe219e1048bd3777f35" | ||
| 17 | } | ||
| 18 | } | ||
corpus/word-lists/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/word-lists/cold/scripts/read.ps1 created+144| ... | @@ -0,0 +1,144 @@ | ||
| 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 | # A paragraph's file is named by its paragraph, one on the page by itself. | ||
| 103 | $owner = if ($file.ParentNode.LocalName -eq 'Page') { $file } else { $file.ParentNode } | ||
| 104 | $payloads += @{ page = $id; object = $owner.GetAttribute('objectID'); | ||
| 105 | kind = $file.LocalName; name = $file.GetAttribute('preferredName'); | ||
| 106 | sha256 = $hash; bytes = $bytes.Length } | ||
| 107 | } | ||
| 108 | $index++ | ||
| 109 | } | ||
| 110 | [IO.File]::WriteAllText((Join-Path $output 'payloads.json'), (ConvertTo-Json -InputObject $payloads -Depth 4), [Text.Encoding]::UTF8) | ||
| 111 | $all = '' | ||
| 112 | $app.GetHierarchy($notebookId, 4, [ref]$all, 1) | ||
| 113 | [xml]$finalTree = $all | ||
| 114 | $finalIds = @($finalTree.SelectNodes('//*[local-name()="Page"]') | ForEach-Object { $_.GetAttribute('ID') } | Sort-Object -Unique) | ||
| 115 | if ($finalIds.Count -ne $pages.Count -or @($finalIds | Where-Object { -not $pages.ContainsKey($_) }).Count -ne 0) { | ||
| 116 | throw 'The notebook hierarchy changed while collecting page evidence; repeat the cold read.' | ||
| 117 | } | ||
| 118 | [IO.File]::WriteAllText((Join-Path $output 'hierarchy.xml'), $all, [Text.Encoding]::UTF8) | ||
| 119 | Write-Output "Read $($sections.Count) sections and $($pages.Count) pages." | ||
| 120 | } catch { | ||
| 121 | $failure = $_ | ||
| 122 | [IO.File]::WriteAllText((Join-Path $output 'failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | ||
| 123 | throw | ||
| 124 | } finally { | ||
| 125 | try { | ||
| 126 | try { | ||
| 127 | if ($notebookId -and $CloneHost) { $app.SyncHierarchy($notebookId) } | ||
| 128 | if ($notebookId -and -not $KeepOpen -and (-not $UseCurrentCache -or $CloneHost)) { | ||
| 129 | $app.CloseNotebook($notebookId, $false) | ||
| 130 | } | ||
| 131 | } catch { | ||
| 132 | if ($null -eq $failure) { throw } | ||
| 133 | [IO.File]::WriteAllText((Join-Path $output 'cleanup-failure.txt'), ($_ | Out-String), [Text.Encoding]::UTF8) | ||
| 134 | } | ||
| 135 | } finally { | ||
| 136 | [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($app) | ||
| 137 | } | ||
| 138 | $app = $null | ||
| 139 | [GC]::Collect() | ||
| 140 | [GC]::WaitForPendingFinalizers() | ||
| 141 | if (-not $UseCurrentCache -and -not $CloneHost) { | ||
| 142 | Get-Process ONENOTE -ErrorAction SilentlyContinue | Wait-Process -Timeout 10 | ||
| 143 | } | ||
| 144 | } | ||
corpus/word-lists/cold/source.json created+14| ... | @@ -0,0 +1,14 @@ | ||
| 1 | [ | ||
| 2 | { | ||
| 3 | "path": "Open Notebook.onetoc2", | ||
| 4 | "bytes": 1596, | ||
| 5 | "sha256": "2974f1efd4b392b00d5d1a3e2485ad34d049e1ea46c0aceaed754f2ff365bc4e", | ||
| 6 | "mtime_ns": 1790967579952579436 | ||
| 7 | }, | ||
| 8 | { | ||
| 9 | "path": "word.one", | ||
| 10 | "bytes": 9632, | ||
| 11 | "sha256": "144bab7f0967f85881a858efd183c54aa8f334f07043d6959a68569b1fcba664", | ||
| 12 | "mtime_ns": 1790967579952375355 | ||
| 13 | } | ||
| 14 | ] | ||
corpus/word-lists/cold/teardown.json created+1| ... | @@ -0,0 +1 @@ | ||
| 1 | {"absent": true} | ||
crates/canvas/src/editor/clip.rs+6-8| ... | @@ -307,15 +307,13 @@ fn outline( | ... | @@ -307,15 +307,13 @@ fn outline( |
| 307 | use super::format::AUTOMATIC; | 307 | use super::format::AUTOMATIC; |
| 308 | for node in nodes { | 308 | for node in nodes { |
| 309 | let mut line = format!("{prefix}{}", " ".repeat(node.level as usize - 1)); | 309 | let mut line = format!("{prefix}{}", " ".repeat(node.level as usize - 1)); |
| 310 | match node.lists.last().map(|list| &definitions[list].kind) { | 310 | if let Some(kind @ Kind::List { .. }) = |
| 311 | Some(Kind::List { | 311 | node.lists.last().map(|list| &definitions[list].kind) |
| 312 | bullet: Some(_), .. | 312 | { |
| 313 | }) => line.push_str("• "), | 313 | match super::html::list_tag(kind) { |
| 314 | Some(kind @ Kind::List { .. }) => { | 314 | ("ul", _) => line.push_str("• "), |
| 315 | let (_, kind) = super::html::list_tag(kind); | 315 | (_, kind) => line.push_str(&format!("{kind}. ")), |
| 316 | line.push_str(&format!("{kind}. ")); | ||
| 317 | } | 316 | } |
| 318 | _ => {} | ||
| 319 | } | 317 | } |
| 320 | let mut tags = node.tags.clone(); | 318 | let mut tags = node.tags.clone(); |
| 321 | match &node.content { | 319 | match &node.content { |
crates/canvas/src/editor/html.rs+200-10| ... | @@ -104,7 +104,10 @@ pub(super) fn list_tag(kind: &Kind) -> (&'static str, &'static str) { | ... | @@ -104,7 +104,10 @@ pub(super) fn list_tag(kind: &Kind) -> (&'static str, &'static str) { |
| 104 | let Kind::List { bullet, format, .. } = kind else { | 104 | let Kind::List { bullet, format, .. } = kind else { |
| 105 | return ("ul", "disc"); | 105 | return ("ul", "disc"); |
| 106 | }; | 106 | }; |
| 107 | if bullet.is_some() { | 107 | let sequence = format |
| 108 | .as_deref() | ||
| 109 | .and_then(|format| format.split('\u{fffd}').nth(1)?.chars().next()); | ||
| 110 | if bullet.is_some() || sequence.is_none() { | ||
| 108 | let kind = match ListStyle::of(kind) { | 111 | let kind = match ListStyle::of(kind) { |
| 109 | Some(ListStyle::Bullet(3)) => "circle", | 112 | Some(ListStyle::Bullet(3)) => "circle", |
| 110 | Some(ListStyle::Bullet(11 | 14)) => "square", | 113 | Some(ListStyle::Bullet(11 | 14)) => "square", |
| ... | @@ -112,9 +115,6 @@ pub(super) fn list_tag(kind: &Kind) -> (&'static str, &'static str) { | ... | @@ -112,9 +115,6 @@ pub(super) fn list_tag(kind: &Kind) -> (&'static str, &'static str) { |
| 112 | }; | 115 | }; |
| 113 | return ("ul", kind); | 116 | return ("ul", kind); |
| 114 | } | 117 | } |
| 115 | let sequence = format | ||
| 116 | .as_deref() | ||
| 117 | .and_then(|format| format.split('\u{fffd}').nth(1)?.chars().next()); | ||
| 118 | let kind = match sequence.map(u32::from) { | 118 | let kind = match sequence.map(u32::from) { |
| 119 | Some(1) => "I", | 119 | Some(1) => "I", |
| 120 | Some(2) => "i", | 120 | Some(2) => "i", |
| ... | @@ -257,6 +257,8 @@ pub fn html_pieces( | ... | @@ -257,6 +257,8 @@ pub fn html_pieces( |
| 257 | tables: Vec::new(), | 257 | tables: Vec::new(), |
| 258 | lists: Vec::new(), | 258 | lists: Vec::new(), |
| 259 | item: None, | 259 | item: None, |
| 260 | word: None, | ||
| 261 | sequences: BTreeMap::new(), | ||
| 260 | open: Vec::new(), | 262 | open: Vec::new(), |
| 261 | runs: Vec::new(), | 263 | runs: Vec::new(), |
| 262 | shown: false, | 264 | shown: false, |
| ... | @@ -267,13 +269,22 @@ pub fn html_pieces( | ... | @@ -267,13 +269,22 @@ pub fn html_pieces( |
| 267 | reader.pieces | 269 | reader.pieces |
| 268 | } | 270 | } |
| 269 | 271 | ||
| 272 | /// A paragraph Word marks as a list item, its `mso-list` list and level, and the marker Word | ||
| 273 | /// wrote before its text, in the marker's font. | ||
| 274 | struct WordItem { | ||
| 275 | list: String, | ||
| 276 | level: usize, | ||
| 277 | marker: String, | ||
| 278 | font: Option<String>, | ||
| 279 | } | ||
| 280 | |||
| 270 | /// A paragraph or table read, before it becomes a clip's. | 281 | /// A paragraph or table read, before it becomes a clip's. |
| 271 | enum Block { | 282 | enum Block { |
| 272 | Text { | 283 | Text { |
| 273 | runs: Vec<(String, Format)>, | 284 | runs: Vec<(String, Format)>, |
| 274 | /// How many lists it sits in, its own not counted. | 285 | /// How many lists it sits in, its own not counted. |
| 275 | depth: usize, | 286 | depth: usize, |
| 276 | list: Option<ListStyle>, | 287 | list: Option<Box<Definition>>, |
| 277 | }, | 288 | }, |
| 278 | Table { | 289 | Table { |
| 279 | rows: Vec<Vec<Vec<Block>>>, | 290 | rows: Vec<Vec<Vec<Block>>>, |
| ... | @@ -296,6 +307,8 @@ struct Open { | ... | @@ -296,6 +307,8 @@ struct Open { |
| 296 | name: String, | 307 | name: String, |
| 297 | format: Format, | 308 | format: Format, |
| 298 | link: Option<usize>, | 309 | link: Option<usize>, |
| 310 | /// Whether it holds a Word list item's marker (`mso-list:Ignore`). | ||
| 311 | marker: bool, | ||
| 299 | } | 312 | } |
| 300 | 313 | ||
| 301 | struct Reader<F> { | 314 | struct Reader<F> { |
| ... | @@ -307,6 +320,10 @@ struct Reader<F> { | ... | @@ -307,6 +320,10 @@ struct Reader<F> { |
| 307 | lists: Vec<ListStyle>, | 320 | lists: Vec<ListStyle>, |
| 308 | /// The list of the item whose text has yet to come. | 321 | /// The list of the item whose text has yet to come. |
| 309 | item: Option<ListStyle>, | 322 | item: Option<ListStyle>, |
| 323 | /// The Word list item being read, as `mso-list` names it. | ||
| 324 | word: Option<WordItem>, | ||
| 325 | /// The number sequence each Word list level (`mso-list` list and level) began with. | ||
| 326 | sequences: BTreeMap<(String, usize), char>, | ||
| 310 | open: Vec<Open>, | 327 | open: Vec<Open>, |
| 311 | runs: Vec<(String, Format)>, | 328 | runs: Vec<(String, Format)>, |
| 312 | /// Whether the paragraph read so far shows anything. | 329 | /// Whether the paragraph read so far shows anything. |
| ... | @@ -366,6 +383,10 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { | ... | @@ -366,6 +383,10 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { |
| 366 | let end = tag_end(after); | 383 | let end = tag_end(after); |
| 367 | let tag = &after[..end]; | 384 | let tag = &after[..end]; |
| 368 | rest = after.get(end + 1..).unwrap_or(""); | 385 | rest = after.get(end + 1..).unwrap_or(""); |
| 386 | // Declarations and Word's `<![if ...]>` conditions. | ||
| 387 | if tag.starts_with('!') { | ||
| 388 | continue; | ||
| 389 | } | ||
| 369 | let (closing, tag) = match tag.strip_prefix('/') { | 390 | let (closing, tag) = match tag.strip_prefix('/') { |
| 370 | Some(tag) => (true, tag), | 391 | Some(tag) => (true, tag), |
| 371 | None => (false, tag), | 392 | None => (false, tag), |
| ... | @@ -394,6 +415,25 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { | ... | @@ -394,6 +415,25 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { |
| 394 | if block || matches!(name, "br" | "table" | "tr" | "td" | "th" | "ul" | "ol") { | 415 | if block || matches!(name, "br" | "table" | "tr" | "td" | "th" | "ul" | "ol") { |
| 395 | self.paragraph(); | 416 | self.paragraph(); |
| 396 | } | 417 | } |
| 418 | let list = style(tag, "mso-list"); | ||
| 419 | if block && let Some(list) = &list { | ||
| 420 | let mut words = list.split_whitespace(); | ||
| 421 | if let (Some(id), Some(Ok(level))) = ( | ||
| 422 | words.next(), | ||
| 423 | words | ||
| 424 | .next() | ||
| 425 | .and_then(|level| level.strip_prefix("level")) | ||
| 426 | .map(str::parse::<usize>), | ||
| 427 | ) && level > 0 | ||
| 428 | { | ||
| 429 | self.word = Some(WordItem { | ||
| 430 | list: id.to_owned(), | ||
| 431 | level, | ||
| 432 | marker: String::new(), | ||
| 433 | font: None, | ||
| 434 | }); | ||
| 435 | } | ||
| 436 | } | ||
| 397 | match name { | 437 | match name { |
| 398 | "br" => return, | 438 | "br" => return, |
| 399 | "img" => { | 439 | "img" => { |
| ... | @@ -479,6 +519,7 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { | ... | @@ -479,6 +519,7 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { |
| 479 | name: name.to_owned(), | 519 | name: name.to_owned(), |
| 480 | format, | 520 | format, |
| 481 | link, | 521 | link, |
| 522 | marker: list.is_some_and(|list| list.eq_ignore_ascii_case("ignore")), | ||
| 482 | }); | 523 | }); |
| 483 | } | 524 | } |
| 484 | 525 | ||
| ... | @@ -547,6 +588,16 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { | ... | @@ -547,6 +588,16 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { |
| 547 | 588 | ||
| 548 | fn text(&mut self, text: &str) { | 589 | fn text(&mut self, text: &str) { |
| 549 | let format = self.format(); | 590 | let format = self.format(); |
| 591 | if self.open.iter().any(|open| open.marker) { | ||
| 592 | if let Some(item) = &mut self.word { | ||
| 593 | let shown = text.trim_matches(|c: char| c.is_whitespace() || c == '\u{a0}'); | ||
| 594 | if !shown.is_empty() && item.marker.is_empty() { | ||
| 595 | item.font = format.font.clone(); | ||
| 596 | } | ||
| 597 | item.marker.push_str(shown); | ||
| 598 | } | ||
| 599 | return; | ||
| 600 | } | ||
| 550 | for c in text.chars() { | 601 | for c in text.chars() { |
| 551 | if c.is_whitespace() && c != '\u{a0}' { | 602 | if c.is_whitespace() && c != '\u{a0}' { |
| 552 | if self.shown && self.space.is_none() { | 603 | if self.shown && self.space.is_none() { |
| ... | @@ -580,15 +631,78 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { | ... | @@ -580,15 +631,78 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { |
| 580 | for open in &mut self.open { | 631 | for open in &mut self.open { |
| 581 | open.link = None; | 632 | open.link = None; |
| 582 | } | 633 | } |
| 583 | let list = self.item.take(); | ||
| 584 | let depth = self.lists.len() - usize::from(list.is_some() && !self.lists.is_empty()); | ||
| 585 | runs.retain(|(text, _)| !text.is_empty()); | 634 | runs.retain(|(text, _)| !text.is_empty()); |
| 586 | if runs.is_empty() { | 635 | if runs.is_empty() { |
| 587 | runs.push((String::new(), self.format())); | 636 | runs.push((String::new(), self.format())); |
| 588 | } | 637 | } |
| 638 | let (list, depth) = match self.word.take() { | ||
| 639 | Some(item) => { | ||
| 640 | let depth = self.lists.len() + item.level - 1; | ||
| 641 | (Some(Box::new(self.word_list(item, &runs[0].1))), depth) | ||
| 642 | } | ||
| 643 | None => { | ||
| 644 | let list = self.item.take(); | ||
| 645 | let depth = | ||
| 646 | self.lists.len() - usize::from(list.is_some() && !self.lists.is_empty()); | ||
| 647 | ( | ||
| 648 | list.map(|style| Box::new(list_definition(style, &runs[0].1))), | ||
| 649 | depth, | ||
| 650 | ) | ||
| 651 | } | ||
| 652 | }; | ||
| 589 | self.sink().push(Block::Text { runs, depth, list }); | 653 | self.sink().push(Block::Text { runs, depth, list }); |
| 590 | } | 654 | } |
| 591 | 655 | ||
| 656 | /// The list OneNote 2010 made of a Word list item whose text begins in `format` (lab, | ||
| 657 | /// 2026-10-02): a marker without a number is a bullet in Word's glyph and font; a number | ||
| 658 | /// keeps Word's punctuation, in the sequence its list level began with. | ||
| 659 | fn word_list(&mut self, item: WordItem, format: &Format) -> Definition { | ||
| 660 | let mut definition = list_definition(ListStyle::NUMBER, format); | ||
| 661 | let Kind::List { | ||
| 662 | font, format: list, .. | ||
| 663 | } = &mut definition.kind | ||
| 664 | else { | ||
| 665 | unreachable!("a number is a list") | ||
| 666 | }; | ||
| 667 | let marker = item.marker; | ||
| 668 | let alphanumeric = |c: char| c.is_ascii_alphanumeric(); | ||
| 669 | let token = marker | ||
| 670 | .find(alphanumeric) | ||
| 671 | .zip(marker.rfind(alphanumeric)) | ||
| 672 | .map(|(start, end)| start..end + 1); | ||
| 673 | let digits = token | ||
| 674 | .as_ref() | ||
| 675 | .is_some_and(|token| marker[token.clone()].bytes().any(|b| b.is_ascii_digit())); | ||
| 676 | let Some(number) = token.filter(|token| digits || token.len() < marker.len()) else { | ||
| 677 | *font = item.font; | ||
| 678 | *list = Some(marker); | ||
| 679 | return definition; | ||
| 680 | }; | ||
| 681 | let token = &marker[number.clone()]; | ||
| 682 | let sequence = *self | ||
| 683 | .sequences | ||
| 684 | .entry((item.list, item.level)) | ||
| 685 | .or_insert_with(|| { | ||
| 686 | let roman = token == "i" | ||
| 687 | || token == "I" | ||
| 688 | || token.len() > 1 && token.chars().all(|c| "ivxlcdmIVXLCDM".contains(c)); | ||
| 689 | let upper = token.starts_with(|c: char| c.is_ascii_uppercase()); | ||
| 690 | match (digits, roman, upper) { | ||
| 691 | (true, ..) => '\u{0}', | ||
| 692 | (_, true, true) => '\u{1}', | ||
| 693 | (_, true, false) => '\u{2}', | ||
| 694 | (_, false, true) => '\u{3}', | ||
| 695 | (_, false, false) => '\u{4}', | ||
| 696 | } | ||
| 697 | }); | ||
| 698 | *list = Some(format!( | ||
| 699 | "{}\u{fffd}{sequence}{}", | ||
| 700 | &marker[..number.start], | ||
| 701 | &marker[number.end..] | ||
| 702 | )); | ||
| 703 | definition | ||
| 704 | } | ||
| 705 | |||
| 592 | /// Where a block read goes: the open cell, else the clip. | 706 | /// Where a block read goes: the open cell, else the clip. |
| 593 | fn sink(&mut self) -> &mut Vec<Block> { | 707 | fn sink(&mut self) -> &mut Vec<Block> { |
| 594 | match self.tables.last_mut() { | 708 | match self.tables.last_mut() { |
| ... | @@ -643,11 +757,10 @@ fn nodes( | ... | @@ -643,11 +757,10 @@ fn nodes( |
| 643 | let level = (depth(&block) - least + 1) as u32; | 757 | let level = (depth(&block) - least + 1) as u32; |
| 644 | let mut paragraph = match block { | 758 | let mut paragraph = match block { |
| 645 | Block::Text { runs, list, .. } => { | 759 | Block::Text { runs, list, .. } => { |
| 646 | let first = runs[0].1.clone(); | ||
| 647 | let mut paragraph = node(Paragraph::from_runs(runs), Format::default())?; | 760 | let mut paragraph = node(Paragraph::from_runs(runs), Format::default())?; |
| 648 | if let Some(style) = list { | 761 | if let Some(list) = list { |
| 649 | let id = new_id()?; | 762 | let id = new_id()?; |
| 650 | definitions.insert(id, list_definition(style, &first)); | 763 | definitions.insert(id, *list); |
| 651 | paragraph.lists = vec![id]; | 764 | paragraph.lists = vec![id]; |
| 652 | } | 765 | } |
| 653 | paragraph | 766 | paragraph |
| ... | @@ -1064,6 +1177,83 @@ mod tests { | ... | @@ -1064,6 +1177,83 @@ mod tests { |
| 1064 | ); | 1177 | ); |
| 1065 | } | 1178 | } |
| 1066 | 1179 | ||
| 1180 | /// Word 2010's list paragraphs (`corpus/clipboard/word-2010.html`) paste as the lists | ||
| 1181 | /// OneNote 2010 made of them (lab, 2026-10-02): each level nested under the item above, | ||
| 1182 | /// a bullet in Word's own glyph and font, a number in Word's sequence and punctuation. | ||
| 1183 | #[test] | ||
| 1184 | fn words_lists_paste_as_lists() { | ||
| 1185 | let html = include_str!("../../../../corpus/clipboard/word-2010.html"); | ||
| 1186 | let [clip] = &clips(html)[..] else { | ||
| 1187 | panic!("one clip") | ||
| 1188 | }; | ||
| 1189 | assert_eq!( | ||
| 1190 | clip.outline(), | ||
| 1191 | [ | ||
| 1192 | "• Bullet one", | ||
| 1193 | " • Bullet nested", | ||
| 1194 | " • Bullet deeper", | ||
| 1195 | "• Bullet two", | ||
| 1196 | "Plain paragraph", | ||
| 1197 | "1. Number one", | ||
| 1198 | " a. Number nested", | ||
| 1199 | " i. Number deeper", | ||
| 1200 | "1. Number two", | ||
| 1201 | "Last plain", | ||
| 1202 | ] | ||
| 1203 | ); | ||
| 1204 | let lists = crate::document::leaves(&clip.paragraphs, None) | ||
| 1205 | .filter_map( | ||
| 1206 | |(.., node)| match &clip.definitions[node.lists.first()?].kind { | ||
| 1207 | Kind::List { | ||
| 1208 | font, | ||
| 1209 | format, | ||
| 1210 | bullet, | ||
| 1211 | .. | ||
| 1212 | } => Some((font.clone(), format.clone().unwrap(), *bullet)), | ||
| 1213 | _ => None, | ||
| 1214 | }, | ||
| 1215 | ) | ||
| 1216 | .collect::<Vec<_>>(); | ||
| 1217 | let bullet = |font: &str, glyph: &str| (Some(font.to_owned()), glyph.to_owned(), None); | ||
| 1218 | let number = |format: &str| (None, format.to_owned(), None); | ||
| 1219 | assert_eq!( | ||
| 1220 | lists, | ||
| 1221 | [ | ||
| 1222 | bullet("Symbol", "\u{b7}"), | ||
| 1223 | bullet("Courier New", "o"), | ||
| 1224 | bullet("Wingdings", "\u{a7}"), | ||
| 1225 | bullet("Symbol", "\u{b7}"), | ||
| 1226 | number("\u{fffd}\u{0}."), | ||
| 1227 | number("\u{fffd}\u{4}."), | ||
| 1228 | number("\u{fffd}\u{2}."), | ||
| 1229 | number("\u{fffd}\u{0}."), | ||
| 1230 | ] | ||
| 1231 | ); | ||
| 1232 | } | ||
| 1233 | |||
| 1234 | /// LibreOffice writes lists as nested `ul` and `ol` around paragraphs, leaving items | ||
| 1235 | /// unclosed. | ||
| 1236 | #[test] | ||
| 1237 | fn libreoffices_lists_paste_as_lists() { | ||
| 1238 | let clip = &clips( | ||
| 1239 | "<ul>\n\t<li><p style=\"margin-bottom: 0in\">Bullet one</p>\n\t<ul>\n\t\t\ | ||
| 1240 | <li><p>Bullet nested</p>\n\t</ul>\n\t<li><p>Bullet two</p>\n</ul>\n<ol>\n\t\ | ||
| 1241 | <li><p>Number one</p>\n\t<ol type=\"a\">\n\t\t<li><p>Number nested</p>\n\t\ | ||
| 1242 | </ol>\n\t<li><p>Number two</p>\n</ol>", | ||
| 1243 | )[0]; | ||
| 1244 | assert_eq!( | ||
| 1245 | clip.outline(), | ||
| 1246 | [ | ||
| 1247 | "• Bullet one", | ||
| 1248 | " • Bullet nested", | ||
| 1249 | "• Bullet two", | ||
| 1250 | "1. Number one", | ||
| 1251 | " a. Number nested", | ||
| 1252 | "1. Number two", | ||
| 1253 | ] | ||
| 1254 | ); | ||
| 1255 | } | ||
| 1256 | |||
| 1067 | /// Snowbound's HTML reads back as the clip it was written from. | 1257 | /// Snowbound's HTML reads back as the clip it was written from. |
| 1068 | #[test] | 1258 | #[test] |
| 1069 | fn snowbound_html_reads_back_as_written() { | 1259 | fn snowbound_html_reads_back_as_written() { |
crates/canvas/tests/clip_paste.rs+72-2| ... | @@ -16,6 +16,7 @@ use onestore::{ | ... | @@ -16,6 +16,7 @@ use onestore::{ |
| 16 | }; | 16 | }; |
| 17 | 17 | ||
| 18 | const ONENOTE: &str = include_str!("../../../corpus/clipboard/onenote-2010.html"); | 18 | const ONENOTE: &str = include_str!("../../../corpus/clipboard/onenote-2010.html"); |
| 19 | const WORD: &str = include_str!("../../../corpus/clipboard/word-2010.html"); | ||
| 19 | 20 | ||
| 20 | /// Each paragraph of a body outline: its level, list kind, text and bold runs. | 21 | /// Each paragraph of a body outline: its level, list kind, text and bold runs. |
| 21 | fn outline(page: &Page, outline: &Outline) -> Vec<String> { | 22 | fn outline(page: &Page, outline: &Outline) -> Vec<String> { |
| ... | @@ -23,8 +24,9 @@ fn outline(page: &Page, outline: &Outline) -> Vec<String> { | ... | @@ -23,8 +24,9 @@ fn outline(page: &Page, outline: &Outline) -> Vec<String> { |
| 23 | for node in nodes { | 24 | for node in nodes { |
| 24 | let list = match node.lists.last().map(|id| &page.definitions[id].kind) { | 25 | let list = match node.lists.last().map(|id| &page.definitions[id].kind) { |
| 25 | Some(Kind::List { | 26 | Some(Kind::List { |
| 26 | bullet: Some(_), .. | 27 | format: Some(format), |
| 27 | }) => "• ", | 28 | .. |
| 29 | }) if !format.contains('\u{fffd}') => "• ", | ||
| 28 | Some(Kind::List { .. }) => "# ", | 30 | Some(Kind::List { .. }) => "# ", |
| 29 | _ => "", | 31 | _ => "", |
| 30 | }; | 32 | }; |
| ... | @@ -310,3 +312,71 @@ fn several_lines_pasted_from_the_title_go_into_the_body() { | ... | @@ -310,3 +312,71 @@ fn several_lines_pasted_from_the_title_go_into_the_body() { |
| 310 | .unwrap(); | 312 | .unwrap(); |
| 311 | } | 313 | } |
| 312 | } | 314 | } |
| 315 | |||
| 316 | /// Word 2010's lists (`corpus/clipboard/word-2010.html`) paste as lists that the section | ||
| 317 | /// stores and reads back. `CANVAS_WORD_LIST_EXPORT` names a directory receiving the section | ||
| 318 | /// as a notebook, for a cold reopen in OneNote 2010. | ||
| 319 | #[test] | ||
| 320 | fn words_lists_paste_and_store() { | ||
| 321 | let source = onestore::create_section("word.one", "Word lists", "Author").unwrap(); | ||
| 322 | let arena = Arena::default(); | ||
| 323 | let mut section = Section::open(&arena, source.clone()).unwrap(); | ||
| 324 | let (space, ..) = section.pages().unwrap()[0].clone(); | ||
| 325 | let mut engine = TextEngine::default(); | ||
| 326 | let mut editor = CanvasEditor::from_page(section.page(space).unwrap(), &mut engine).unwrap(); | ||
| 327 | editor | ||
| 328 | .place_caret(&mut engine, [72.0, 120.0], 400.0) | ||
| 329 | .unwrap(); | ||
| 330 | editor | ||
| 331 | .paste_pieces(&mut engine, html_pieces(WORD, 0x409, |_, _| None)) | ||
| 332 | .unwrap(); | ||
| 333 | let ops = editor.take_ops().unwrap(); | ||
| 334 | let ops = ops.into_iter().map(|op| Op::Page { space, op }).collect(); | ||
| 335 | section | ||
| 336 | .apply( | ||
| 337 | "Author", | ||
| 338 | &Edit { | ||
| 339 | at: 133_000_000_000_000_000, | ||
| 340 | ops, | ||
| 341 | }, | ||
| 342 | ) | ||
| 343 | .unwrap(); | ||
| 344 | |||
| 345 | let mut image = source; | ||
| 346 | section.seal().unwrap().unwrap().apply(&mut image).unwrap(); | ||
| 347 | let arena = Arena::default(); | ||
| 348 | let reopened = Section::open(&arena, image.clone()).unwrap(); | ||
| 349 | let page = reopened.page(space).unwrap(); | ||
| 350 | assert_eq!( | ||
| 351 | bodies(&page), | ||
| 352 | [ | ||
| 353 | vec!["Word lists"], | ||
| 354 | vec![ | ||
| 355 | "• Bullet one", | ||
| 356 | " • Bullet nested", | ||
| 357 | " • Bullet deeper", | ||
| 358 | "• Bullet two", | ||
| 359 | "Plain paragraph", | ||
| 360 | "# Number one", | ||
| 361 | " # Number nested", | ||
| 362 | " # Number deeper", | ||
| 363 | "# Number two", | ||
| 364 | "Last plain", | ||
| 365 | ] | ||
| 366 | ] | ||
| 367 | ); | ||
| 368 | assert_eq!(bodies(&page), bodies(&editor.page().unwrap())); | ||
| 369 | |||
| 370 | if let Some(directory) = std::env::var_os("CANVAS_WORD_LIST_EXPORT") { | ||
| 371 | let directory = std::path::PathBuf::from(directory); | ||
| 372 | std::fs::create_dir_all(&directory).unwrap(); | ||
| 373 | std::fs::write(directory.join("word.one"), &image).unwrap(); | ||
| 374 | let file_id = Store::parse(&image).unwrap().header.file_id; | ||
| 375 | std::fs::write( | ||
| 376 | directory.join("Open Notebook.onetoc2"), | ||
| 377 | onestore::create_table_of_contents("Open Notebook.onetoc2", &[("word.one", file_id)]) | ||
| 378 | .unwrap(), | ||
| 379 | ) | ||
| 380 | .unwrap(); | ||
| 381 | } | ||
| 382 | } |
tools/test_word_lists.py created+55| ... | @@ -0,0 +1,55 @@ | ||
| 1 | from pathlib import Path | ||
| 2 | import re | ||
| 3 | import runpy | ||
| 4 | import shutil | ||
| 5 | from tempfile import TemporaryDirectory | ||
| 6 | import unittest | ||
| 7 | import xml.etree.ElementTree as ET | ||
| 8 | |||
| 9 | from native_xml import ns | ||
| 10 | |||
| 11 | ROOT = Path(__file__).resolve().parent.parent | ||
| 12 | FIXTURE = ROOT / 'corpus/word-lists' | ||
| 13 | compare = runpy.run_path(str(ROOT / 'tools/verify-document.py'))['compare'] | ||
| 14 | |||
| 15 | |||
| 16 | class WordListsTest(unittest.TestCase): | ||
| 17 | def test_lists_pasted_from_word_open_in_onenote_as_its_own_paste_gave_them(self): | ||
| 18 | read = FIXTURE / 'cold/read' | ||
| 19 | with TemporaryDirectory() as temporary: | ||
| 20 | copy = Path(temporary) / 'read' | ||
| 21 | shutil.copytree(read, copy) | ||
| 22 | compare(FIXTURE / 'candidate', copy) | ||
| 23 | page = ET.parse(read / 'page-000.xml').getroot() | ||
| 24 | lines = [] | ||
| 25 | |||
| 26 | def walk(children, depth): | ||
| 27 | for element in children.findall('one:OE', ns): | ||
| 28 | bullet = element.find('one:List/one:Bullet', ns) | ||
| 29 | number = element.find('one:List/one:Number', ns) | ||
| 30 | marker = ('bullet %s ' % bullet.get('bullet') if bullet is not None | ||
| 31 | else number.get('text') + ' ' if number is not None else '') | ||
| 32 | text = re.sub(r'<[^>]*>', '', element.find('one:T', ns).text or '') | ||
| 33 | lines.append(' ' * depth + marker + text) | ||
| 34 | for nested in element.findall('one:OEChildren', ns): | ||
| 35 | walk(nested, depth + 1) | ||
| 36 | |||
| 37 | outline = page.findall('one:Outline', ns)[-1] | ||
| 38 | for children in outline.findall('one:OEChildren', ns): | ||
| 39 | walk(children, 0) | ||
| 40 | self.assertEqual(lines, [ | ||
| 41 | 'bullet 1 Bullet one', | ||
| 42 | ' bullet 0 Bullet nested', | ||
| 43 | ' bullet 13 Bullet deeper', | ||
| 44 | 'bullet 1 Bullet two', | ||
| 45 | 'Plain paragraph', | ||
| 46 | '1. Number one', | ||
| 47 | ' a. Number nested', | ||
| 48 | ' i. Number deeper', | ||
| 49 | '2. Number two', | ||
| 50 | 'Last plain', | ||
| 51 | ]) | ||
| 52 | |||
| 53 | |||
| 54 | if __name__ == '__main__': | ||
| 55 | unittest.main() | ||