| 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 | 9 | ANSI text, an enhanced metafile and a device-independent bitmap, but no RTF. The |
| 10 | 10 | tag is absent from the HTML. `crates/canvas/src/editor/html.rs` reads it in its |
| 11 | 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 | 307 | use super::format::AUTOMATIC; |
| 308 | 308 | for node in nodes { |
| 309 | 309 | let mut line = format!("{prefix}{}", " ".repeat(node.level as usize - 1)); |
| 310 | match node.lists.last().map(|list| &definitions[list].kind) { | |
| 311 | Some(Kind::List { | |
| 312 | bullet: Some(_), .. | |
| 313 | }) => line.push_str("• "), | |
| 314 | Some(kind @ Kind::List { .. }) => { | |
| 315 | let (_, kind) = super::html::list_tag(kind); | |
| 316 | line.push_str(&format!("{kind}. ")); | |
| 310 | if let Some(kind @ Kind::List { .. }) = | |
| 311 | node.lists.last().map(|list| &definitions[list].kind) | |
| 312 | { | |
| 313 | match super::html::list_tag(kind) { | |
| 314 | ("ul", _) => line.push_str("• "), | |
| 315 | (_, kind) => line.push_str(&format!("{kind}. ")), | |
| 317 | 316 | } |
| 318 | _ => {} | |
| 319 | 317 | } |
| 320 | 318 | let mut tags = node.tags.clone(); |
| 321 | 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 | 104 | let Kind::List { bullet, format, .. } = kind else { |
| 105 | 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 | 111 | let kind = match ListStyle::of(kind) { |
| 109 | 112 | Some(ListStyle::Bullet(3)) => "circle", |
| 110 | 113 | Some(ListStyle::Bullet(11 | 14)) => "square", |
| ... | ... | @@ -112,9 +115,6 @@ pub(super) fn list_tag(kind: &Kind) -> (&'static str, &'static str) { |
| 112 | 115 | }; |
| 113 | 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 | 118 | let kind = match sequence.map(u32::from) { |
| 119 | 119 | Some(1) => "I", |
| 120 | 120 | Some(2) => "i", |
| ... | ... | @@ -257,6 +257,8 @@ pub fn html_pieces( |
| 257 | 257 | tables: Vec::new(), |
| 258 | 258 | lists: Vec::new(), |
| 259 | 259 | item: None, |
| 260 | word: None, | |
| 261 | sequences: BTreeMap::new(), | |
| 260 | 262 | open: Vec::new(), |
| 261 | 263 | runs: Vec::new(), |
| 262 | 264 | shown: false, |
| ... | ... | @@ -267,13 +269,22 @@ pub fn html_pieces( |
| 267 | 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 | 281 | /// A paragraph or table read, before it becomes a clip's. |
| 271 | 282 | enum Block { |
| 272 | 283 | Text { |
| 273 | 284 | runs: Vec<(String, Format)>, |
| 274 | 285 | /// How many lists it sits in, its own not counted. |
| 275 | 286 | depth: usize, |
| 276 | list: Option<ListStyle>, | |
| 287 | list: Option<Box<Definition>>, | |
| 277 | 288 | }, |
| 278 | 289 | Table { |
| 279 | 290 | rows: Vec<Vec<Vec<Block>>>, |
| ... | ... | @@ -296,6 +307,8 @@ struct Open { |
| 296 | 307 | name: String, |
| 297 | 308 | format: Format, |
| 298 | 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 | 314 | struct Reader<F> { |
| ... | ... | @@ -307,6 +320,10 @@ struct Reader<F> { |
| 307 | 320 | lists: Vec<ListStyle>, |
| 308 | 321 | /// The list of the item whose text has yet to come. |
| 309 | 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 | 327 | open: Vec<Open>, |
| 311 | 328 | runs: Vec<(String, Format)>, |
| 312 | 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 | 383 | let end = tag_end(after); |
| 367 | 384 | let tag = &after[..end]; |
| 368 | 385 | rest = after.get(end + 1..).unwrap_or(""); |
| 386 | // Declarations and Word's `<![if ...]>` conditions. | |
| 387 | if tag.starts_with('!') { | |
| 388 | continue; | |
| 389 | } | |
| 369 | 390 | let (closing, tag) = match tag.strip_prefix('/') { |
| 370 | 391 | Some(tag) => (true, tag), |
| 371 | 392 | None => (false, tag), |
| ... | ... | @@ -394,6 +415,25 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { |
| 394 | 415 | if block || matches!(name, "br" | "table" | "tr" | "td" | "th" | "ul" | "ol") { |
| 395 | 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 | 437 | match name { |
| 398 | 438 | "br" => return, |
| 399 | 439 | "img" => { |
| ... | ... | @@ -479,6 +519,7 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { |
| 479 | 519 | name: name.to_owned(), |
| 480 | 520 | format, |
| 481 | 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 | 588 | |
| 548 | 589 | fn text(&mut self, text: &str) { |
| 549 | 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 | 601 | for c in text.chars() { |
| 551 | 602 | if c.is_whitespace() && c != '\u{a0}' { |
| 552 | 603 | if self.shown && self.space.is_none() { |
| ... | ... | @@ -580,15 +631,78 @@ impl<F: FnMut(&str, [Option<f32>; 2]) -> Option<Piece>> Reader<F> { |
| 580 | 631 | for open in &mut self.open { |
| 581 | 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 | 634 | runs.retain(|(text, _)| !text.is_empty()); |
| 586 | 635 | if runs.is_empty() { |
| 587 | 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 | 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 | 706 | /// Where a block read goes: the open cell, else the clip. |
| 593 | 707 | fn sink(&mut self) -> &mut Vec<Block> { |
| 594 | 708 | match self.tables.last_mut() { |
| ... | ... | @@ -643,11 +757,10 @@ fn nodes( |
| 643 | 757 | let level = (depth(&block) - least + 1) as u32; |
| 644 | 758 | let mut paragraph = match block { |
| 645 | 759 | Block::Text { runs, list, .. } => { |
| 646 | let first = runs[0].1.clone(); | |
| 647 | 760 | let mut paragraph = node(Paragraph::from_runs(runs), Format::default())?; |
| 648 | if let Some(style) = list { | |
| 761 | if let Some(list) = list { | |
| 649 | 762 | let id = new_id()?; |
| 650 | definitions.insert(id, list_definition(style, &first)); | |
| 763 | definitions.insert(id, *list); | |
| 651 | 764 | paragraph.lists = vec![id]; |
| 652 | 765 | } |
| 653 | 766 | paragraph |
| ... | ... | @@ -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 | 1257 | /// Snowbound's HTML reads back as the clip it was written from. |
| 1068 | 1258 | #[test] |
| 1069 | 1259 | fn snowbound_html_reads_back_as_written() { |
crates/canvas/tests/clip_paste.rs+72-2| ... | ... | @@ -16,6 +16,7 @@ use onestore::{ |
| 16 | 16 | }; |
| 17 | 17 | |
| 18 | 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 | 21 | /// Each paragraph of a body outline: its level, list kind, text and bold runs. |
| 21 | 22 | fn outline(page: &Page, outline: &Outline) -> Vec<String> { |
| ... | ... | @@ -23,8 +24,9 @@ fn outline(page: &Page, outline: &Outline) -> Vec<String> { |
| 23 | 24 | for node in nodes { |
| 24 | 25 | let list = match node.lists.last().map(|id| &page.definitions[id].kind) { |
| 25 | 26 | Some(Kind::List { |
| 26 | bullet: Some(_), .. | |
| 27 | }) => "• ", | |
| 27 | format: Some(format), | |
| 28 | .. | |
| 29 | }) if !format.contains('\u{fffd}') => "• ", | |
| 28 | 30 | Some(Kind::List { .. }) => "# ", |
| 29 | 31 | _ => "", |
| 30 | 32 | }; |
| ... | ... | @@ -310,3 +312,71 @@ fn several_lines_pasted_from_the_title_go_into_the_body() { |
| 310 | 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() |