diff --git a/CHANGELOG.md b/CHANGELOG.md index a3c0fa143..1d7053f23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,24 @@ follow semantic versioning; release dates are ISO 8601. ### Public API +- **Enter in a DOCX continues the text it follows.** Word gives a new paragraph its mark's + formatting, and the export left the mark of most paragraphs — every one written at an exact + line height — in the document's own size and face. Pressing Enter at the end of a paragraph + and typing set the new letters in another size: 9pt after 10.5pt in `NavySidebar`, 14pt after + 11pt in `ClassicInvoice`. Typing at the end of a document set them on the last paragraph's + exact lines at the document's size, 14pt letters on lines set for 10pt text. Each paragraph's + mark now carries what its text ends in — face, size, colour, weight, slant, decoration and + tracking, as its last run has them, a rich list item's included; a paragraph ending in a + chip keeps its own style there, since the chip's letters are styled for a fill the mark does + not carry. Measured with the Word + editing protocol over the 62 corpus documents: + - Enter and typing continues in the text's size and face in all 62, against 37; + - text typed at a document's end stands as the page sets its last paragraph in 6 more. + + The remaining "type at the end" failures are documents ending in a table, where the typing + goes into its last cell. A numbering marker that its level states no style for now takes the + item's — the style the page draws it in — rather than the document's. Word's corpus lines are + unchanged; LibreOffice sets 20 lines up to 0.2pt lower, none across. - **A DOCX panel in the body starts its text the whole padding in.** A panel's table took half its left border off its cell's left margin and its indent, for an editor that keeps the border inside the cell; for a panel in the body, Word 16 and LibreOffice on Windows centre diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ec8e58903..526b697c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -380,11 +380,13 @@ Choose the smallest tests that match the change: It records what it can decide alone in `edit-protocol-corpus.json`: - text kept, and a paragraph grown or shrunk and still above the block after it; - the body size following Normal; - - typed text standing below everything else, outside any table, visible, on lines it fits. + - typed text standing below everything else, outside any table, visible, in the size of the + text it continues (or, after an empty paragraph, on lines that fit it). It reads the letters' size and visibility from the text, without the paragraph mark. Beside the lengthened copy and the new-page copy it writes PDFs, from which a person judges whether - a page still looks right. It takes about a minute a document. + a page still looks right. It takes about a minute a document; `-Scenario` runs only the + scenarios named, by the names the JSON records. If a change affects public docs, examples, or screenshots, update those assets in the same PR so the repository stays internally consistent. diff --git a/assets/readme/examples/word-export-companion.docx b/assets/readme/examples/word-export-companion.docx index 647103153..d1c8545af 100644 Binary files a/assets/readme/examples/word-export-companion.docx and b/assets/readme/examples/word-export-companion.docx differ diff --git a/docs/recipes/docx-export.md b/docs/recipes/docx-export.md index 07fbab713..eeeafb2db 100644 --- a/docs/recipes/docx-export.md +++ b/docs/recipes/docx-export.md @@ -353,6 +353,13 @@ paragraph and lines 1.08 tall, and the page has neither. A paragraph's space aro written where the page has some. A reader who wants Word's airier defaults back can set them on `Normal`. +Each paragraph's mark — the character closing it — carries what its text ends in: face, size, +colour, weight, slant, decoration and tracking, written as its last run's are. It does so on a +line of exact height too, which the mark does not grow. Word continues from the mark: press +Enter at the end of a paragraph and type, and the new paragraph is in the text's formatting, +not the document's own; type at the end of the document, and the letters take its last +paragraph's. + A paragraph that declared an outline level — `bookmark(new DocumentBookmarkOptions(name, level))` — also carries Word's own `HeadingN` style, which is what fills the Navigation Pane, the outline view and a generated table of contents. The style states the outline diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv index 5e2015a9a..a8ef2fa80 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-linux-lines.tsv @@ -909,7 +909,7 @@ cv-navy_sidebar 0:18f8b50d02d8 0.06 0.10 developingandexecutingdata-drive cv-navy_sidebar 0:2adca2422cbe 0.08 0.10 growth,increaseengagement,anddel cv-navy_sidebar 0:b3c222da2e1d 0.10 0.10 digitalmarketing,marketresearch, cv-navy_sidebar 0:53e5e7c5a884 -0.23 0.10 experience -cv-navy_sidebar 0:8a39b996b27c -0.06 0.16 jan2022-present +cv-navy_sidebar 0:8a39b996b27c 0.14 0.16 jan2022-present cv-navy_sidebar 0:f97eeb2fe120 -0.20 0.10 brightwavesolutions,newyork,ny cv-navy_sidebar 0:f97eeb2fe120#1 -0.22 0.10 brightwavesolutions,newyork,ny cv-navy_sidebar 0:f97eeb2fe120#2 -0.25 0.10 brightwavesolutions,newyork,ny @@ -920,7 +920,7 @@ cv-navy_sidebar 0:94d0b8d13763 -0.45 0.09 anddesignteamstoalignstrategies. cv-navy_sidebar 0:5dabf21df91b -0.45 0.10 •analyzecampaignperformanceusing cv-navy_sidebar 0:1cb810ca6189 -0.45 0.09 optimizeroiandreporting. cv-navy_sidebar 0:3b172b9ab75e -0.26 0.10 marketingspecialist -cv-navy_sidebar 0:d514872487af -0.08 0.15 jun2020-dec2021 +cv-navy_sidebar 0:d514872487af 0.12 0.15 jun2020-dec2021 cv-navy_sidebar 0:a13f57f4d861 -0.47 0.10 •executeddigitalmarketingcampaig cv-navy_sidebar 0:4b308a27b878 -0.47 0.09 channels. cv-navy_sidebar 0:39b87fda4a82 -0.47 0.10 •conductedmarketresearchandcompe @@ -928,7 +928,7 @@ cv-navy_sidebar 0:b088780ed500 -0.47 0.09 andinformstrategy. cv-navy_sidebar 0:bafd8ea30ebe -0.47 0.10 •createdengagingcontentforblogs, cv-navy_sidebar 0:88a47597543f -0.47 0.09 engagementby40%. cv-navy_sidebar 0:dcf0600524d2 -0.29 0.10 marketingintern -cv-navy_sidebar 0:73a85d4a71b0 -0.11 0.13 jan2020-may2020 +cv-navy_sidebar 0:73a85d4a71b0 0.09 0.13 jan2020-may2020 cv-navy_sidebar 0:e31c3dfebb27 -0.25 0.10 •assistedinplanningandexecutingm cv-navy_sidebar 0:f7ee178c4b93 -0.25 0.10 •supportedcontentcreationandsoci cv-navy_sidebar 0:3ffec90452c5 -0.25 0.10 •analyzeddataandpreparedreportst @@ -1256,7 +1256,7 @@ cv-serif_headline 0:7fd0d2d0fde8 -0.37 0.12 backendservices.proficientinjava cv-serif_headline 0:81bb94512f0b -0.38 0.12 systemdesign,anddeliveringhigh-q cv-serif_headline 0:53e5e7c5a884 0.00 0.13 experience cv-serif_headline 0:b37c6a2d5d11 0.04 0.10 seniorsoftwareengineer -cv-serif_headline 0:e7227bbe0c40 0.04 0.76 jan2022–present +cv-serif_headline 0:e7227bbe0c40 0.14 0.76 jan2022–present cv-serif_headline 0:f4e5af202d87 0.10 0.10 techsolutionsinc.|newyork,ny cv-serif_headline 0:e6f39d8143bc -0.15 0.10 •designedanddevelopedmicroservic cv-serif_headline 0:f41ff924454c -0.15 0.10 over1mactiveusers. @@ -1264,14 +1264,14 @@ cv-serif_headline 0:4b16c2d8cb52 0.10 0.10 •implementedrestfulapisandintegr cv-serif_headline 0:5fd82b15227c -0.14 0.10 •optimizeddatabasequeriesandappl cv-serif_headline 0:38a7ed6f25a8 -0.15 0.10 35%. cv-serif_headline 0:df1ebc2b61e1 0.11 0.10 •ledateamof4engineersandmentored -cv-serif_headline 0:ee96c91a5683 0.20 0.80 jun2019–dec2021 +cv-serif_headline 0:ee96c91a5683 0.30 0.80 jun2019–dec2021 cv-serif_headline 0:01bb3d9e4f47 0.26 0.10 innovatechltd.|newyork,ny cv-serif_headline 0:1e69401eddeb 0.27 0.10 •builtscalablewebapplicationsusi cv-serif_headline 0:babe9476fc8f 0.28 0.10 •collaboratedwithcross-functiona cv-serif_headline 0:23cd6cbea0ca 0.28 0.10 •implementedci/cdpipelinesusingj cv-serif_headline 0:45cca445fa04 0.29 0.10 •improvedcodequalityandreducedpr cv-serif_headline 0:78b24f507c79 0.39 0.10 juniorsoftwareengineer -cv-serif_headline 0:07b9e4fd0073 0.38 0.81 jun2018–may2019 +cv-serif_headline 0:07b9e4fd0073 0.48 0.81 jun2018–may2019 cv-serif_headline 0:2198cdea3aba 0.44 0.10 devsphere|newyork,ny cv-serif_headline 0:173ed9f7b6b1 0.45 0.10 •assistedindevelopingandmaintain cv-serif_headline 0:0a1fbe0c94d9 0.45 0.10 •wroteunitandintegrationteststoe @@ -2031,16 +2031,16 @@ invoice-consulting 0:3b45ab243edb 0.51 0.10 +61398765432 invoice-consulting 0:a17a9fc6d4a8 0.51 0.10 hello@northpoint.com.au invoice-consulting 0:ef0664c1d352 0.51 0.10 northpoint.com.au invoice-consulting 0:52d6e3de4fa0 -0.44 0.07 invoice -invoice-consulting 0:6282391a4cc1 -0.63 0.07 invoicenumber: +invoice-consulting 0:6282391a4cc1 -0.53 0.07 invoicenumber: invoice-consulting 0:7d66eb3c1070 -0.19 0.06 inv-2025-0478 invoice-consulting 0:7d66eb3c1070#1 0.96 0.10 inv-2025-0478 -invoice-consulting 0:89973ab74e1c -0.63 0.07 issuedate: +invoice-consulting 0:89973ab74e1c -0.53 0.07 issuedate: invoice-consulting 0:0dee636efa5b -0.19 0.06 26may2025 -invoice-consulting 0:0e0fb0fc0939 -0.63 0.07 duedate: +invoice-consulting 0:0e0fb0fc0939 -0.53 0.07 duedate: invoice-consulting 0:7709d5dcefbe -0.19 0.06 25june2025 -invoice-consulting 0:633dcfcb4a68 -0.63 0.07 project: +invoice-consulting 0:633dcfcb4a68 -0.53 0.07 project: invoice-consulting 0:28447bbc85e1 -0.19 0.06 digitalstrategyengagement -invoice-consulting 0:d7523d00af19 -0.63 0.07 ponumber: +invoice-consulting 0:d7523d00af19 -0.53 0.07 ponumber: invoice-consulting 0:80a06a9ef12d -0.19 0.06 po-7892 invoice-consulting 0:aef9b6cb1bcd -0.06 0.10 billedto invoice-consulting 0:3fc5213307f9 -0.10 0.10 greenfieldindustriesltd. @@ -2092,9 +2092,9 @@ invoice-consulting 0:55907ef405f6 0.94 0.82 1,880.00 invoice-consulting 0:55907ef405f6#1 0.94 0.87 1,880.00 invoice-consulting 0:f2e12ad6cc9f 0.67 0.39 presentation&finalreport invoice-consulting 0:50de6dc61b80 0.70 0.39 executivepresentationanddelivery -invoice-consulting 0:d6577e95efba -0.26 0.12 subtotal +invoice-consulting 0:d6577e95efba -0.16 0.12 subtotal invoice-consulting 0:6311b1798398 0.18 0.63 14,000.00 -invoice-consulting 0:e370855bde0c -0.26 0.12 gst(10%) +invoice-consulting 0:e370855bde0c -0.16 0.12 gst(10%) invoice-consulting 0:b87e26fb8fac 0.18 0.52 1,400.00 invoice-consulting 0:713a1893d967 -0.92 0.12 totaldue(aud) invoice-consulting 0:d4464a725927 0.05 0.98 15,400.00 @@ -2123,15 +2123,15 @@ invoice-consulting_long 0:3b45ab243edb 0.51 0.10 +61398765432 invoice-consulting_long 0:a17a9fc6d4a8 0.51 0.10 hello@northpoint.com.au invoice-consulting_long 0:ef0664c1d352 0.51 0.10 northpoint.com.au invoice-consulting_long 0:52d6e3de4fa0 -0.44 0.07 invoice -invoice-consulting_long 0:6282391a4cc1 -0.63 0.07 invoicenumber: +invoice-consulting_long 0:6282391a4cc1 -0.53 0.07 invoicenumber: invoice-consulting_long 0:7d66eb3c1070 -0.19 0.06 inv-2025-0478 -invoice-consulting_long 0:89973ab74e1c -0.63 0.07 issuedate: +invoice-consulting_long 0:89973ab74e1c -0.53 0.07 issuedate: invoice-consulting_long 0:0dee636efa5b -0.19 0.06 26may2025 -invoice-consulting_long 0:0e0fb0fc0939 -0.63 0.07 duedate: +invoice-consulting_long 0:0e0fb0fc0939 -0.53 0.07 duedate: invoice-consulting_long 0:7709d5dcefbe -0.19 0.06 25june2025 -invoice-consulting_long 0:633dcfcb4a68 -0.63 0.07 project: +invoice-consulting_long 0:633dcfcb4a68 -0.53 0.07 project: invoice-consulting_long 0:28447bbc85e1 -0.19 0.06 digitalstrategyengagement -invoice-consulting_long 0:d7523d00af19 -0.63 0.07 ponumber: +invoice-consulting_long 0:d7523d00af19 -0.53 0.07 ponumber: invoice-consulting_long 0:80a06a9ef12d -0.19 0.06 po-7892 invoice-consulting_long 0:aef9b6cb1bcd -0.06 0.10 billedto invoice-consulting_long 0:3fc5213307f9 -0.10 0.10 greenfieldindustriesltd. @@ -2327,9 +2327,9 @@ invoice-consulting_long 1:f2e12ad6cc9f 0.38 0.39 presentation&finalreport invoice-consulting_long 1:f2e12ad6cc9f#1 0.48 0.39 presentation&finalreport invoice-consulting_long 1:50de6dc61b80 0.41 0.39 executivepresentationanddelivery invoice-consulting_long 1:50de6dc61b80#1 0.51 0.39 executivepresentationanddelivery -invoice-consulting_long 1:d6577e95efba -0.43 0.12 subtotal +invoice-consulting_long 1:d6577e95efba -0.33 0.12 subtotal invoice-consulting_long 1:6311b1798398 0.01 0.63 14,000.00 -invoice-consulting_long 1:e370855bde0c -0.43 0.12 gst(10%) +invoice-consulting_long 1:e370855bde0c -0.33 0.12 gst(10%) invoice-consulting_long 1:b87e26fb8fac 0.01 0.52 1,400.00 invoice-consulting_long 1:713a1893d967 -1.09 0.12 totaldue(aud) invoice-consulting_long 1:d4464a725927 -0.12 0.98 15,400.00 diff --git a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv index 06df212fc..1d05dd45d 100644 --- a/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv +++ b/qa/src/test/resources/docx-fidelity/libreoffice-windows-lines.tsv @@ -909,7 +909,7 @@ cv-navy_sidebar 0:18f8b50d02d8 0.06 0.10 developingandexecutingdata-drive cv-navy_sidebar 0:2adca2422cbe 0.08 0.10 growth,increaseengagement,anddel cv-navy_sidebar 0:b3c222da2e1d 0.10 0.10 digitalmarketing,marketresearch, cv-navy_sidebar 0:53e5e7c5a884 -0.23 0.10 experience -cv-navy_sidebar 0:8a39b996b27c -0.06 0.16 jan2022-present +cv-navy_sidebar 0:8a39b996b27c 0.14 0.16 jan2022-present cv-navy_sidebar 0:f97eeb2fe120 -0.20 0.10 brightwavesolutions,newyork,ny cv-navy_sidebar 0:f97eeb2fe120#1 -0.22 0.10 brightwavesolutions,newyork,ny cv-navy_sidebar 0:f97eeb2fe120#2 -0.25 0.10 brightwavesolutions,newyork,ny @@ -920,7 +920,7 @@ cv-navy_sidebar 0:94d0b8d13763 -0.45 0.09 anddesignteamstoalignstrategies. cv-navy_sidebar 0:5dabf21df91b -0.45 0.10 •analyzecampaignperformanceusing cv-navy_sidebar 0:1cb810ca6189 -0.45 0.09 optimizeroiandreporting. cv-navy_sidebar 0:3b172b9ab75e -0.26 0.10 marketingspecialist -cv-navy_sidebar 0:d514872487af -0.08 0.15 jun2020-dec2021 +cv-navy_sidebar 0:d514872487af 0.12 0.15 jun2020-dec2021 cv-navy_sidebar 0:a13f57f4d861 -0.47 0.10 •executeddigitalmarketingcampaig cv-navy_sidebar 0:4b308a27b878 -0.47 0.09 channels. cv-navy_sidebar 0:39b87fda4a82 -0.47 0.10 •conductedmarketresearchandcompe @@ -928,7 +928,7 @@ cv-navy_sidebar 0:b088780ed500 -0.47 0.09 andinformstrategy. cv-navy_sidebar 0:bafd8ea30ebe -0.47 0.10 •createdengagingcontentforblogs, cv-navy_sidebar 0:88a47597543f -0.47 0.09 engagementby40%. cv-navy_sidebar 0:dcf0600524d2 -0.29 0.10 marketingintern -cv-navy_sidebar 0:73a85d4a71b0 -0.11 0.18 jan2020-may2020 +cv-navy_sidebar 0:73a85d4a71b0 0.09 0.18 jan2020-may2020 cv-navy_sidebar 0:e31c3dfebb27 -0.25 0.10 •assistedinplanningandexecutingm cv-navy_sidebar 0:f7ee178c4b93 -0.25 0.10 •supportedcontentcreationandsoci cv-navy_sidebar 0:3ffec90452c5 -0.25 0.10 •analyzeddataandpreparedreportst @@ -1155,7 +1155,7 @@ cv-serif_headline 0:7fd0d2d0fde8 -0.37 0.12 backendservices.proficientinjava cv-serif_headline 0:81bb94512f0b -0.38 0.12 systemdesign,anddeliveringhigh-q cv-serif_headline 0:53e5e7c5a884 0.00 0.13 experience cv-serif_headline 0:b37c6a2d5d11 0.04 0.10 seniorsoftwareengineer -cv-serif_headline 0:e7227bbe0c40 0.04 0.76 jan2022–present +cv-serif_headline 0:e7227bbe0c40 0.14 0.76 jan2022–present cv-serif_headline 0:f4e5af202d87 0.10 0.10 techsolutionsinc.|newyork,ny cv-serif_headline 0:e6f39d8143bc -0.15 0.10 •designedanddevelopedmicroservic cv-serif_headline 0:f41ff924454c -0.15 0.10 over1mactiveusers. @@ -1163,14 +1163,14 @@ cv-serif_headline 0:4b16c2d8cb52 0.10 0.10 •implementedrestfulapisandintegr cv-serif_headline 0:5fd82b15227c -0.14 0.10 •optimizeddatabasequeriesandappl cv-serif_headline 0:38a7ed6f25a8 -0.15 0.10 35%. cv-serif_headline 0:df1ebc2b61e1 0.11 0.10 •ledateamof4engineersandmentored -cv-serif_headline 0:ee96c91a5683 0.20 0.85 jun2019–dec2021 +cv-serif_headline 0:ee96c91a5683 0.30 0.85 jun2019–dec2021 cv-serif_headline 0:01bb3d9e4f47 0.26 0.10 innovatechltd.|newyork,ny cv-serif_headline 0:1e69401eddeb 0.27 0.10 •builtscalablewebapplicationsusi cv-serif_headline 0:babe9476fc8f 0.28 0.10 •collaboratedwithcross-functiona cv-serif_headline 0:23cd6cbea0ca 0.28 0.10 •implementedci/cdpipelinesusingj cv-serif_headline 0:45cca445fa04 0.29 0.10 •improvedcodequalityandreducedpr cv-serif_headline 0:78b24f507c79 0.39 0.10 juniorsoftwareengineer -cv-serif_headline 0:07b9e4fd0073 0.38 0.86 jun2018–may2019 +cv-serif_headline 0:07b9e4fd0073 0.48 0.86 jun2018–may2019 cv-serif_headline 0:2198cdea3aba 0.44 0.10 devsphere|newyork,ny cv-serif_headline 0:173ed9f7b6b1 0.45 0.10 •assistedindevelopingandmaintain cv-serif_headline 0:0a1fbe0c94d9 0.45 0.10 •wroteunitandintegrationteststoe @@ -1930,16 +1930,16 @@ invoice-consulting 0:3b45ab243edb 0.51 0.10 +61398765432 invoice-consulting 0:a17a9fc6d4a8 0.51 0.10 hello@northpoint.com.au invoice-consulting 0:ef0664c1d352 0.51 0.10 northpoint.com.au invoice-consulting 0:52d6e3de4fa0 -0.44 0.07 invoice -invoice-consulting 0:6282391a4cc1 -0.63 0.07 invoicenumber: +invoice-consulting 0:6282391a4cc1 -0.53 0.07 invoicenumber: invoice-consulting 0:7d66eb3c1070 -0.19 0.06 inv-2025-0478 invoice-consulting 0:7d66eb3c1070#1 1.01 0.10 inv-2025-0478 -invoice-consulting 0:89973ab74e1c -0.63 0.07 issuedate: +invoice-consulting 0:89973ab74e1c -0.53 0.07 issuedate: invoice-consulting 0:0dee636efa5b -0.19 0.06 26may2025 -invoice-consulting 0:0e0fb0fc0939 -0.63 0.07 duedate: +invoice-consulting 0:0e0fb0fc0939 -0.53 0.07 duedate: invoice-consulting 0:7709d5dcefbe -0.19 0.06 25june2025 -invoice-consulting 0:633dcfcb4a68 -0.63 0.07 project: +invoice-consulting 0:633dcfcb4a68 -0.53 0.07 project: invoice-consulting 0:28447bbc85e1 -0.19 0.06 digitalstrategyengagement -invoice-consulting 0:d7523d00af19 -0.63 0.07 ponumber: +invoice-consulting 0:d7523d00af19 -0.53 0.07 ponumber: invoice-consulting 0:80a06a9ef12d -0.19 0.06 po-7892 invoice-consulting 0:aef9b6cb1bcd -0.06 0.10 billedto invoice-consulting 0:3fc5213307f9 -0.10 0.10 greenfieldindustriesltd. @@ -1991,9 +1991,9 @@ invoice-consulting 0:55907ef405f6 0.94 0.22 1,880.00 invoice-consulting 0:55907ef405f6#1 0.94 0.27 1,880.00 invoice-consulting 0:f2e12ad6cc9f 0.72 -0.21 presentation&finalreport invoice-consulting 0:50de6dc61b80 0.75 -0.21 executivepresentationanddelivery -invoice-consulting 0:d6577e95efba -0.21 0.12 subtotal +invoice-consulting 0:d6577e95efba -0.11 0.12 subtotal invoice-consulting 0:6311b1798398 0.23 0.63 14,000.00 -invoice-consulting 0:e370855bde0c -0.21 0.12 gst(10%) +invoice-consulting 0:e370855bde0c -0.11 0.12 gst(10%) invoice-consulting 0:b87e26fb8fac 0.23 0.57 1,400.00 invoice-consulting 0:713a1893d967 -0.87 0.12 totaldue(aud) invoice-consulting 0:d4464a725927 0.10 1.03 15,400.00 @@ -2022,15 +2022,15 @@ invoice-consulting_long 0:3b45ab243edb 0.51 0.10 +61398765432 invoice-consulting_long 0:a17a9fc6d4a8 0.51 0.10 hello@northpoint.com.au invoice-consulting_long 0:ef0664c1d352 0.51 0.10 northpoint.com.au invoice-consulting_long 0:52d6e3de4fa0 -0.44 0.07 invoice -invoice-consulting_long 0:6282391a4cc1 -0.63 0.07 invoicenumber: +invoice-consulting_long 0:6282391a4cc1 -0.53 0.07 invoicenumber: invoice-consulting_long 0:7d66eb3c1070 -0.19 0.06 inv-2025-0478 -invoice-consulting_long 0:89973ab74e1c -0.63 0.07 issuedate: +invoice-consulting_long 0:89973ab74e1c -0.53 0.07 issuedate: invoice-consulting_long 0:0dee636efa5b -0.19 0.06 26may2025 -invoice-consulting_long 0:0e0fb0fc0939 -0.63 0.07 duedate: +invoice-consulting_long 0:0e0fb0fc0939 -0.53 0.07 duedate: invoice-consulting_long 0:7709d5dcefbe -0.19 0.06 25june2025 -invoice-consulting_long 0:633dcfcb4a68 -0.63 0.07 project: +invoice-consulting_long 0:633dcfcb4a68 -0.53 0.07 project: invoice-consulting_long 0:28447bbc85e1 -0.19 0.06 digitalstrategyengagement -invoice-consulting_long 0:d7523d00af19 -0.63 0.07 ponumber: +invoice-consulting_long 0:d7523d00af19 -0.53 0.07 ponumber: invoice-consulting_long 0:80a06a9ef12d -0.19 0.06 po-7892 invoice-consulting_long 0:aef9b6cb1bcd -0.06 0.10 billedto invoice-consulting_long 0:3fc5213307f9 -0.10 0.10 greenfieldindustriesltd. @@ -2226,9 +2226,9 @@ invoice-consulting_long 1:f2e12ad6cc9f 0.38 -0.21 presentation&finalreport invoice-consulting_long 1:f2e12ad6cc9f#1 0.48 -0.21 presentation&finalreport invoice-consulting_long 1:50de6dc61b80 0.41 -0.21 executivepresentationanddelivery invoice-consulting_long 1:50de6dc61b80#1 0.51 -0.21 executivepresentationanddelivery -invoice-consulting_long 1:d6577e95efba -0.38 0.12 subtotal +invoice-consulting_long 1:d6577e95efba -0.28 0.12 subtotal invoice-consulting_long 1:6311b1798398 0.06 0.63 14,000.00 -invoice-consulting_long 1:e370855bde0c -0.38 0.12 gst(10%) +invoice-consulting_long 1:e370855bde0c -0.28 0.12 gst(10%) invoice-consulting_long 1:b87e26fb8fac 0.06 0.57 1,400.00 invoice-consulting_long 1:713a1893d967 -1.04 0.12 totaldue(aud) invoice-consulting_long 1:d4464a725927 -0.07 1.03 15,400.00 diff --git a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java index fe62c2aa9..748a5e5ba 100644 --- a/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java +++ b/render-docx/src/main/java/com/demcha/compose/document/backend/semantic/docx/DocxSemanticBackend.java @@ -2734,9 +2734,10 @@ private BigInteger numberingFor(XWPFDocument document, followTheMarkerWithASpace(level); } if (measured && list.textStyle() != null) { - // Word draws the marker in the paragraph mark's style, and an item at an exact - // line leaves its mark the document's: a list set smaller, or in another face, - // would get a marker wider than the one the column was measured against. + // Word draws the marker in the paragraph mark's style. A numbered item is a list + // line in the list's style, so its mark is too (styleTheMark); the level states + // the list's face, size and colour as well, the ones the column was measured + // against. The page draws the marker in the list's style, decoration included. applyDefaultRunProperties(level.addNewRPr(), list.textStyle()); } } @@ -3284,15 +3285,24 @@ private void writeRichListLine(XWPFDocument document, DocumentTextStyle style, } } } + // The mark takes what the item's text ends in, its last run's style where it has one. + DocumentTextStyle markStyle = style; if (item.isRich()) { pictures = pictures.max(writeInlineTextRuns(para, style, item.runs(), path, line)); + for (InlineRun run : item.runs()) { + InlineTextRun text = textOf(run); + if (text != null) { + // A chip's letters are styled for a fill the mark does not carry. + markStyle = text.textStyle() == null || backgroundOf(run) != null ? style : text.textStyle(); + } + } } else { XWPFRun label = para.createRun(); applyStyle(label, style); setTextBrokenAtLines(label, item.label()); } makeRoomForPictures(para, pictures); - styleTheMark(para, style); + styleTheMark(para, markStyle); } /** How far past a list marker's picture its tab stop must stand for the tab to reach it, in points. */ @@ -6066,51 +6076,58 @@ private void oweWhatTheLinesFallShort(XWPFParagraph target, ParagraphNode source private static final double LINES_SHORT_TOLERANCE = 0.1; /** - * Sets a paragraph's mark in its text's size and face, where the editor may grow the line. + * Sets a paragraph's mark in its text's character formatting: face, size, colour, weight, + * slant, decoration and tracking, as {@link #applyStyle} writes them on its runs. * - *

The mark closing a paragraph is a character on its last line, and its size counts - * towards that line's height. Left unstyled it takes the document's own size and face, so a - * line of half-point text — a coloured cell a hairline tall, the way a heading rule is drawn - * in a table — came out as tall as a line of body text in both editors: {@code SlateOrange}'s - * rules under its credentials headings were 8pt bars. A line written at an exact height - * does not grow for the mark and is left alone; one the layout did not measure, and one - * written at least a picture's height, are the ones it reaches. Called once the line's rule - * is settled, pictures included.

+ *

The mark is what Word continues from. Press Enter at the end of a paragraph and type, + * and the new paragraph's letters take the mark's formatting. Type at the end of a document, + * and the letters take its last paragraph's. A mark left unstyled takes the document's own + * size and face. Word's editing protocol, run over the corpus, found the new paragraph in + * another size in 25 of 62 documents: 9pt after 10.5pt in {@code NavySidebar}, 14pt after + * 11pt in {@code ClassicInvoice}. In 6 more, what was typed at the end stood at the + * document's size on the last paragraph's exact lines, which were set for smaller text: + * 14pt letters on lines set for 10pt. A paragraph ending in a chip keeps its own style on + * the mark: the chip's letters are styled for its fill, which the mark does not carry.

+ * + *

The mark is also a character on the paragraph's last line, and its size counts towards + * that line's height where the line is not exact. A line of half-point text — a coloured + * cell a hairline tall, the way a heading rule is drawn in a table — came out as tall as a + * line of body text in both editors while the mark was in the document's size: + * {@code SlateOrange}'s rules under its credentials headings were 8pt bars. Called once the + * line's rule is settled, pictures included.

* * @param target the paragraph, its runs written - * @param style the text style of the paragraph's text + * @param style the text style of the paragraph's text, its last run's where they differ */ private void styleTheMark(XWPFParagraph target, DocumentTextStyle style) { - if (style == null || !(style.size() > 0) || hasAnExactLine(target)) { + if (style == null || !(style.size() > 0)) { return; } - DocumentTextStyle defaults = documentDefaultStyle; - boolean sameSize = defaults != null && Math.round(style.size() * HALF_POINTS_PER_POINT) - == Math.round(defaults.size() * HALF_POINTS_PER_POINT); - String family = wordFamilyOf(style.fontName()); - boolean sameFace = family == null - || defaults != null && family.equals(wordFamilyOf(defaults.fontName())); - if (sameSize && sameFace) { + // Styled as a run would be, on a run of no paragraph's, and moved onto the mark: one + // reading of the style, the same properties the text carries, and none it does not. + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR scratch = + org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR.Factory.newInstance(); + applyStyle(new XWPFRun(scratch, (IRunBody) target), style); + if (!scratch.isSetRPr()) { return; } + CTRPr text = scratch.getRPr(); CTPPr properties = target.getCTP().isSetPPr() ? target.getCTP().getPPr() : target.getCTP().addNewPPr(); org.openxmlformats.schemas.wordprocessingml.x2006.main.CTParaRPr mark = properties.isSetRPr() ? properties.getRPr() : properties.addNewRPr(); - if (!sameFace) { - // The face the runs are set in, the way applyStyle names it on them: a mark in the - // document's face grows the line by that face's height, not the text's. - org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFonts fonts = - mark.sizeOfRFontsArray() > 0 ? mark.getRFontsArray(0) : mark.addNewRFonts(); - fonts.setAscii(family); - fonts.setHAnsi(family); - fonts.setCs(family); - fonts.setEastAsia(family); - } - if (!sameSize) { - BigInteger halfPoints = BigInteger.valueOf(Math.max(1, Math.round(style.size() * HALF_POINTS_PER_POINT))); - (mark.sizeOfSzArray() > 0 ? mark.getSzArray(0) : mark.addNewSz()).setVal(halfPoints); - (mark.sizeOfSzCsArray() > 0 ? mark.getSzCsArray(0) : mark.addNewSzCs()).setVal(halfPoints); - } + // Through the typed setters, which put each element where the schema orders it. Each + // replaces what the mark held: the mark is set here, before anything else writes on it. + mark.setRFontsArray(text.getRFontsArray()); + mark.setBArray(text.getBArray()); + mark.setBCsArray(text.getBCsArray()); + mark.setIArray(text.getIArray()); + mark.setICsArray(text.getICsArray()); + mark.setStrikeArray(text.getStrikeArray()); + mark.setColorArray(text.getColorArray()); + mark.setSpacingArray(text.getSpacingArray()); + mark.setSzArray(text.getSzArray()); + mark.setSzCsArray(text.getSzCsArray()); + mark.setUArray(text.getUArray()); } /** Whether a paragraph's lines are written at an exact height, which no mark changes. */ @@ -6450,8 +6467,11 @@ private void writeParagraphRuns(XWPFParagraph para, ParagraphNode node, boolean // rest of that sentence rather than something the phrase overrides away. DocumentLinkTarget target = text.linkTarget() != null ? text.linkTarget() : node.linkTarget(); XWPFRun docRun = newRun(para, target); - markStyle = text.textStyle() == null ? node.textStyle() : text.textStyle(); - applyStyle(docRun, markStyle); + DocumentTextStyle runStyle = text.textStyle() == null ? node.textStyle() : text.textStyle(); + // A chip's letters are styled for its fill, which the mark does not carry: white on + // a red badge would continue as white on the page. Its paragraph's style goes on. + markStyle = backgroundOf(run) != null ? node.textStyle() : runStyle; + applyStyle(docRun, runStyle); applyRunDirection(docRun, rightToLeft); applyInlineBackground(docRun, backgroundOf(run), path); setTextBrokenAtLines(docRun, text.text()); diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxParagraphMarkTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxParagraphMarkTest.java index 64a6bc12d..2294ef8b6 100644 --- a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxParagraphMarkTest.java +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxParagraphMarkTest.java @@ -19,13 +19,65 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * A paragraph whose line height the editor sets sizes its mark as its text: the mark is a - * character on the last line and counts towards its height. + * A paragraph's mark carries what its text ends in: it is what Word continues from on Enter, + * and a character on the last line that counts towards a line the editor sets the height of. * * @author Artem Demchyshyn */ class DocxParagraphMarkTest { + @Test + void aParagraphsMarkCarriesWhatItsTextEndsInSoEnterContinuesIt() throws Exception { + // Word gives the paragraph Enter opens its mark's formatting. With the mark left at the + // document's own, the new paragraph's letters came out another size: 9pt after 10.5pt + // in NavySidebar, 14pt after 11pt in ClassicInvoice. Lines written exact are included: + // the mark does not grow them, but it is what typing continues. + DocumentColor accent = DocumentColor.rgb(30, 90, 160); + try (XWPFDocument document = export(page -> page + .addParagraph(p -> p.text("Body text sets the document's size")) + .addParagraph(p -> p + .inlineText("Plain, then ") + .inlineText("bold and coloured", DocumentTextStyle.builder().size(11) + .decoration(com.demcha.compose.document.style.DocumentTextDecoration.BOLD) + .color(accent).build())))) { + XWPFParagraph paragraph = document.getParagraphs().stream() + .filter(p -> p.getText().startsWith("Plain")).findFirst().orElseThrow(); + CTPPr properties = paragraph.getCTP().getPPr(); + + assertThat(properties.getSpacing().getLineRule()).as("an exact line").hasToString("exact"); + var mark = properties.getRPr(); + assertThat(mark.getSzArray(0).getVal()).as("its last run's size, half points").hasToString("22"); + assertThat(mark.sizeOfBArray()).as("bold, as its last run").isEqualTo(1); + Object colour = mark.getColorArray(0).getVal(); + assertThat(colour instanceof byte[] bytes ? java.util.HexFormat.of().withUpperCase().formatHex(bytes) + : String.valueOf(colour)).as("in its colour").isEqualTo("1E5AA0"); + } + } + + @Test + void aRichListItemsMarkCarriesItsLastRunsStyleNotTheLists() throws Exception { + // A rich item writes each run in its own style; Enter after one ending in a larger run + // continues in that run's size, not the list's. + DocumentTextStyle large = DocumentTextStyle.builder().size(13).build(); + try (XWPFDocument document = export(page -> page + .addParagraph(p -> p.text("Body text sets the document's size")) + .addList(list -> list + .bullet() + .hangingIndent(true) + .textStyle(DocumentTextStyle.builder().size(9).build()) + .addItem(rich -> rich.plain("Small, then ").style("large", large))))) { + java.util.List paragraphs = new java.util.ArrayList<>(document.getParagraphs()); + document.getTables().forEach(table -> table.getRows().forEach(row -> + row.getTableCells().forEach(cell -> paragraphs.addAll(cell.getParagraphs())))); + XWPFParagraph item = paragraphs.stream() + .filter(p -> p.getText().contains("Small, then")).findFirst() + .orElseThrow(() -> new AssertionError(paragraphs.stream().map(XWPFParagraph::getText).toList())); + + assertThat(item.getCTP().getPPr().getRPr().getSzArray(0).getVal()) + .as("the last run's 13pt, half points").hasToString("26"); + } + } + @Test void aHairlineOfTextInACellIsNotAsTallAsBodyText() throws Exception { // A heading rule drawn as a filled cell holding half-point text: with the mark at the @@ -45,14 +97,35 @@ void aHairlineOfTextInACellIsNotAsTallAsBodyText() throws Exception { } @Test - void aLineWrittenAtAnExactHeightLeavesItsMarkAlone() throws Exception { + void anExactLineInTheDocumentsOwnStyleLeavesItsMarkSilent() throws Exception { + // The text restates Normal, so its runs say nothing and neither does its mark: a global + // restyle reaches both, and Enter continues in Normal. try (XWPFDocument document = export(page -> page - .addParagraph(p -> p.text("Body")) - .addParagraph(p -> p.text("Small print").textStyle(DocumentTextStyle.DEFAULT.withSize(6))))) { - CTPPr small = document.getParagraphs().get(1).getCTP().getPPr(); + .addParagraph(p -> p.text("Body text sets the document's size")) + .addParagraph(p -> p.text("More body text")))) { + CTPPr properties = document.getParagraphs().get(1).getCTP().getPPr(); + + assertThat(properties.getSpacing().getLineRule()).hasToString("exact"); + assertThat(properties.isSetRPr()).isFalse(); + } + } + + @Test + void aParagraphEndingInAChipContinuesInItsOwnStyleNotTheChips() throws Exception { + // The chip's letters are white for its red fill, which the mark does not carry: Enter + // after it would type white on the page. + DocumentTextStyle onBadge = DocumentTextStyle.builder().size(9).color(DocumentColor.WHITE).build(); + try (XWPFDocument document = export(page -> page + .addParagraph(p -> p.text("Body text sets the document's size")) + .addParagraph(p -> p.inlineText("Status: ") + .inlineHighlight("overdue", onBadge, DocumentColor.rgb(214, 56, 56), 0, + DocumentInsets.zero())))) { + XWPFParagraph paragraph = document.getParagraphs().stream() + .filter(p -> p.getText().startsWith("Status")).findFirst().orElseThrow(); + CTPPr properties = paragraph.getCTP().getPPr(); - assertThat(small.getSpacing().getLineRule()).hasToString("exact"); - assertThat(small.isSetRPr()).isFalse(); + assertThat(properties.isSetRPr() && properties.getRPr().sizeOfColorArray() > 0) + .as("not the chip's white").isFalse(); } } @@ -85,7 +158,13 @@ void aParagraphComposedInACellIsSetAtTheLineThePageMeasured() throws Exception { assertThat(properties.getSpacing().getLineRule()).hasToString("exact"); assertThat(((Number) properties.getSpacing().getLine()).intValue()).as("the face's ascent and descent, no leading").isBetween(16 * 20, 24 * 20); - assertThat(properties.isSetRPr()).as("an exact line needs no mark").isFalse(); + // An exact line does not grow for its mark, but Enter at its end continues from it. + assertThat(properties.getRPr().getSzArray(0).getVal()) + .as("the mark in the text's size, half points").hasToString("40"); + assertThat(properties.getRPr().getRFontsArray(0).getAscii()) + .as("and in its face, as its run names it") + .isEqualTo(document.getTables().get(0).getRow(0).getCell(0).getParagraphs().get(0) + .getRuns().get(0).getFontFamily()); } } diff --git a/scripts/docx-visual/edit-protocol-corpus.ps1 b/scripts/docx-visual/edit-protocol-corpus.ps1 index 28a9a7735..2cb785d8f 100644 --- a/scripts/docx-visual/edit-protocol-corpus.ps1 +++ b/scripts/docx-visual/edit-protocol-corpus.ps1 @@ -25,6 +25,10 @@ One or more exported .docx files. All are copied into the output directory before the first is edited, and only the copies are opened. +.PARAMETER Scenario + The scenarios to run, by the names the protocol records them under; all of them when + left out. + .PARAMETER OutputDir Where the inputs' copies, the edited copies, their PDFs and edit-protocol-corpus.json are written. Keep it outside a module's target directory: a build's clean removes it. @@ -37,10 +41,22 @@ [CmdletBinding()] param( [Parameter(Mandatory = $true)][string[]]$Docx, - [Parameter(Mandatory = $true)][string]$OutputDir + [Parameter(Mandatory = $true)][string]$OutputDir, + [string[]]$Scenario ) $ErrorActionPreference = 'Stop' + +$SCENARIOS = 'lengthen-a-paragraph', 'shorten-a-paragraph', 'insert-a-paragraph', 'restyle-body-through-normal', + 'insert-a-table-row', 'delete-a-table-row', 'continue-a-list', 'add-a-page', 'round-trip' +# `-File` hands "a,b" over as one string: split it, and refuse a name no scenario has rather +# than run nothing and report an empty protocol. +$Scenario = @($Scenario | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim() } | Where-Object { $_ }) +$unknown = @($Scenario | Where-Object { $_ -notin $SCENARIOS }) +if ($unknown.Count -gt 0) { + throw "no scenario is called $($unknown -join ', '); the scenarios are $($SCENARIOS -join ', ')" +} + New-Item -ItemType Directory -Force $OutputDir | Out-Null $OutputDir = (Resolve-Path $OutputDir).Path @@ -61,6 +77,10 @@ $WD_HEADER_FOOTER_EVEN_PAGES = 3 # sets a single line of its faces at 1.15 to 1.2 of the size. $EXACT_LINE_SHARE = 1.1 +# The smallest size typed text is read at, in points: a paragraph that holds only pictures has +# its runs and mark a point tall, and text typed on from it would continue at that point. +$READABLE_POINTS = 4 + $ADDED = ' The editing protocol appended this sentence to make the paragraph about twice as long, so that it has to rewrap and whatever stands below it has to move down to make room for it.' function New-Result { @@ -218,6 +238,7 @@ function Get-FooterOn { # An edit Word refuses is a result of that scenario, not the end of the document's protocol. function Invoke-Scenario { param($Word, $Source, [string]$Name, [scriptblock]$Edit, [scriptblock]$Verify, [switch]$Pdf) + if ($Scenario -and $Name -notin $Scenario) { return $null } try { return Invoke-ScenarioOnce $Word $Source $Name $Edit $Verify -Pdf:$Pdf } catch { @@ -326,7 +347,7 @@ function Invoke-Protocol { $p = $doc.Paragraphs($state.index) $state.style = $p.Style.NameLocal $lastLetter = Get-TextRange $p - $lastLetter.MoveStart(1, $lastLetter.End - $lastLetter.Start - 1) | Out-Null + $lastLetter.Start = $lastLetter.End - 1 $state.size = $lastLetter.Font.Size $state.font = $lastLetter.Font.Name Send-EnterAndType $doc $p 'A paragraph the protocol inserted to see whether it joins the flow' @@ -423,11 +444,18 @@ function Invoke-Protocol { # 7. Type at the end of the document until it runs onto a new page, as a person does: the # caret Word gives the document's end, Enter first where the last paragraph holds text. - # What was typed stands below everything else, outside any table, visible, on lines it - # fits, and the page count follows. The footer the new page shows is recorded. + # What was typed stands below everything else, outside any table, visible, and the page + # count follows. Typing on from a paragraph's text, the letters are in that text's size, + # on the lines the page set it on; typing in an empty paragraph, they fit its lines. The + # footer the new page shows is recorded. $results += Invoke-Scenario $Word $Source 'add-a-page' -Pdf -Edit { param($doc, $state) $last = $doc.Paragraphs($doc.Paragraphs.Count) + if ((Get-Text $last).Length -gt 0) { + $lastLetter = Get-TextRange $last + $lastLetter.Start = $lastLetter.End - 1 + $state.continues = [double]$lastLetter.Font.Size + } $end = $doc.Content $end.Collapse(0) $end.Select() @@ -463,10 +491,14 @@ function Invoke-Protocol { $hidden = $text.Font.Hidden -ne 0 $size = $text.Font.Size $format = $typed.Format - $squeezed = $format.LineSpacingRule -eq $WD_LINE_SPACE_EXACTLY -and $format.LineSpacing -lt $size * $EXACT_LINE_SHARE + # A paragraph's exact lines are set for its own text; typed on at another size, the + # letters stand on lines meant for others. An empty one's lines are judged against the + # letters alone. + $squeezed = [double]$size -lt $READABLE_POINTS -or $(if ($null -ne $state.continues) { [double]$size -ne $state.continues } else { + $format.LineSpacingRule -eq $WD_LINE_SPACE_EXACTLY -and $format.LineSpacing -lt $size * $EXACT_LINE_SHARE }) $footer = Get-FooterOn $doc.Sections($doc.Sections.Count) $lastPage - $detail = "$($state.lines) lines typed, pages $($state.pagesBefore) -> $($state.pagesAfter), the last on page $lastPage at ${size}pt, hidden=$hidden, exact $($format.LineSpacing)pt lines too short=$squeezed, footer there '$footer'" - if ($hidden -or $squeezed) { return New-Result 'add-a-page' 'FAIL' "what was typed at the end cannot be read; $detail" } + $detail = "$($state.lines) lines typed, pages $($state.pagesBefore) -> $($state.pagesAfter), the last on page $lastPage at ${size}pt continuing $($state.continues)pt text, hidden=$hidden, on exact $($format.LineSpacing)pt lines not set for it=$squeezed, footer there '$footer'" + if ($hidden -or $squeezed) { return New-Result 'add-a-page' 'FAIL' "what was typed at the end cannot be read as the page set it; $detail" } if ($state.pagesAfter -le $state.pagesBefore -or $lastPage -ne $state.pagesAfter) { return New-Result 'add-a-page' 'FAIL' $detail } New-Result 'add-a-page' 'PASS' $detail }