From 94aa6c787ab2bfe9a19d3e939d8d89d3a0d524ee Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 4 Oct 2026 22:16:40 +0100
Subject: [PATCH 01/10] fix(docx): set a paragraph's mark in what its text ends
in, so Enter and typing at the end continue it
---
docs/recipes/docx-export.md | 7 ++
.../libreoffice-windows-lines.tsv | 40 +++++-----
.../semantic/docx/DocxSemanticBackend.java | 73 ++++++++++---------
.../semantic/docx/DocxParagraphMarkTest.java | 35 ++++++++-
4 files changed, 100 insertions(+), 55 deletions(-)
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-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..a6d3a7d4a 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
@@ -6066,51 +6066,56 @@ 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.
- *
- * 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.
+ * 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 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 9 more, what was typed at the end stood at the
+ * document's size on the last paragraph's exact lines, which were too short for it:
+ * 14pt letters on 9.25pt lines.
+ *
+ * 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.
+ 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. */
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..cbe299720 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
@@ -26,6 +26,33 @@
*/
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);
+ assertThat(mark.getColorArray(0).getVal()).as("in its colour")
+ .isEqualTo(paragraph.getRuns().get(paragraph.getRuns().size() - 1).getCTR().getRPr().getColorArray(0).getVal());
+ }
+ }
+
@Test
void aHairlineOfTextInACellIsNotAsTallAsBodyText() throws Exception {
// A heading rule drawn as a filled cell holding half-point text: with the mark at the
@@ -85,7 +112,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());
}
}
From 9d49a10eda9b6c55bee6d2313cf0dc6f9974c5f5 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 4 Oct 2026 22:16:40 +0100
Subject: [PATCH 02/10] test(docx): judge text typed at a document's end
against the text it continues, and run chosen scenarios
---
scripts/docx-visual/edit-protocol-corpus.ps1 | 29 ++++++++++++++++----
1 file changed, 23 insertions(+), 6 deletions(-)
diff --git a/scripts/docx-visual/edit-protocol-corpus.ps1 b/scripts/docx-visual/edit-protocol-corpus.ps1
index 28a9a7735..dc212deed 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,7 +41,8 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string[]]$Docx,
- [Parameter(Mandatory = $true)][string]$OutputDir
+ [Parameter(Mandatory = $true)][string]$OutputDir,
+ [string[]]$Scenario
)
$ErrorActionPreference = 'Stop'
@@ -218,6 +223,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 {
@@ -423,11 +429,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.MoveStart(1, $lastLetter.End - $lastLetter.Start - 1) | Out-Null
+ $state.continues = [double]$lastLetter.Font.Size
+ }
$end = $doc.Content
$end.Collapse(0)
$end.Select()
@@ -463,10 +476,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 = 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
}
From 40dd44c641ab99cad633682257d9925bfda98d7f Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 4 Oct 2026 22:32:05 +0100
Subject: [PATCH 03/10] fix(docx): set a rich list item's mark in its last
run's style
---
.../semantic/docx/DocxSemanticBackend.java | 19 +++++++++++----
.../semantic/docx/DocxParagraphMarkTest.java | 24 +++++++++++++++++++
2 files changed, 38 insertions(+), 5 deletions(-)
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 a6d3a7d4a..a754054f7 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,9 @@ 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, which carries what the
+ // item's text ends in (styleTheMark): stated on the level, the marker keeps the
+ // list's own face, size and colour, the ones the column was measured against.
applyDefaultRunProperties(level.addNewRPr(), list.textStyle());
}
}
@@ -3284,15 +3284,23 @@ 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) {
+ markStyle = text.textStyle() == 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. */
@@ -6104,7 +6112,8 @@ private void styleTheMark(XWPFParagraph target, DocumentTextStyle style) {
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();
- // Through the typed setters, which put each element where the schema orders it.
+ // 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());
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 cbe299720..6114a2e26 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
@@ -53,6 +53,30 @@ void aParagraphsMarkCarriesWhatItsTextEndsInSoEnterContinuesIt() throws Exceptio
}
}
+ @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
From 703f9924fb0be16a905a68ec26ec44aca1ea3289 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 4 Oct 2026 22:32:05 +0100
Subject: [PATCH 04/10] test(docx): split and check the scenarios asked for,
and read typed text at a readable size only
---
scripts/docx-visual/edit-protocol-corpus.ps1 | 19 +++++++++++++++++--
1 file changed, 17 insertions(+), 2 deletions(-)
diff --git a/scripts/docx-visual/edit-protocol-corpus.ps1 b/scripts/docx-visual/edit-protocol-corpus.ps1
index dc212deed..1a7e55e8a 100644
--- a/scripts/docx-visual/edit-protocol-corpus.ps1
+++ b/scripts/docx-visual/edit-protocol-corpus.ps1
@@ -46,6 +46,17 @@ param(
)
$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
@@ -66,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 {
@@ -479,8 +494,8 @@ function Invoke-Protocol {
# 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 = 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 }
+ $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 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" }
From ce975147e8270ffac16b0582678a8a5f76763021 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 4 Oct 2026 23:00:54 +0100
Subject: [PATCH 05/10] docs(docx): record that a paragraph's mark carries what
its text ends in, and re-render the Word export preview
---
CHANGELOG.md | 16 ++++++++++++++++
.../examples/word-export-companion.docx | Bin 10130 -> 10198 bytes
2 files changed, 16 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a3c0fa143..d606ca139 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,22 @@ 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. Measured with the Word
+ editing protocol over the 62 corpus documents:
+ - Enter and typing continues in the text's size and face in 61, 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/assets/readme/examples/word-export-companion.docx b/assets/readme/examples/word-export-companion.docx
index 647103153ff64a4449f2855a2bdda421975faa2b..7a27459d8fd6a9e2a3a5b6f1917ea656270a64da 100644
GIT binary patch
delta 3455
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
bQ2^^D%6sv&6j5$q8n#AMfc#YGqRsyRyGd62
From c35747f68842d1105e8a4366c778f73d8b52728f Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 4 Oct 2026 23:31:08 +0100
Subject: [PATCH 06/10] fix(docx): keep a paragraph's own style on its mark
where it ends in a chip
---
.../examples/word-export-companion.docx | Bin 10198 -> 10199 bytes
.../semantic/docx/DocxSemanticBackend.java | 24 ++++++----
.../semantic/docx/DocxParagraphMarkTest.java | 42 +++++++++++++-----
3 files changed, 47 insertions(+), 19 deletions(-)
diff --git a/assets/readme/examples/word-export-companion.docx b/assets/readme/examples/word-export-companion.docx
index 7a27459d8fd6a9e2a3a5b6f1917ea656270a64da..d1c8545af93c3169078a1e790de4dabcd3fe5d07 100644
GIT binary patch
delta 746
zcmccSf8C!az?+#xgn@&DgTZ(AMxF*nCg0tg`xqrb^yHOH=Rs5zvub^`|6>Dz*8itY
zocEpE656`R#AeFr(@J5BIPeO^;svvNIOEqlFT
z!0R_bBA;UzqTc#sZ}c
zWoPzHn)HRQ+V%5_4I6*Fa$0YpSARIHYU%f^?oDStx4xM;HE>M|Q(B!;m+_?+oG5nuqEAhs))EH{3elUBkbHt?Fqe+xk_nGwv{_r$1o2uRJYLt_-qn204MrmI88os2V1pSCe7%n*2shMIK~%{4S3>)r<@bCs-I5fPO{-
u`IGh46~Ss&&-y;42q;qn#HbF`nw+n$0P@*Hbs5Inlh>&$u{o)N%m4t32pzEi
delta 741
zcmccaf6bpKz?+#xgn@&DgMoe9MxF*nCiZQc`xqrb^yHOH=Rs5zvugcWzt;u=Zt#`bXMs
zP7j)NFJXCx6sL51zFtY!J>E>h0>aT0OdbL8Y
z)Zu7`0+-jl-#b*7pS*nYQ&y?&t4KTR^IbNlpRbM#Jg0vu>0i*v_@62^HoF7<^PbAD
zpSJFEpTWe(3U5D)ec#L7_NsKzm&REQg;nN^UAnsFQU%XrA9&UAFHwE*_p5x?y8lOv
z?=_dQHJbmJx_ei&-QjE-{w-`)PcxbFSLJ5@U_0k`fOSFc<4ldRORA>kcOLb>`yg`N
zHmvbV$jyqiZ4Vgbe*N8J@l`#*o1G)wOf{>7kwK85nTde`Ij%PAvHs!$MV6L;7C6$v
z1eFCq{A$IW;K3Tsa*q6rgGkiqDCv<{;{nstqUmHkVki6_feZ%;Z5l
zp8Cw&d$|}Gmeeu;ZAAu(lT*|b6hLYk7fUXB1e96^bdwCS5(Zg;$yNe#d;#8!Od<@h
zWHtGMnhc}oo9hdEBXHWMDYK!oVPeqC0Q0fx04C&FWd-rxXF@YJeEk6`GR^
h)D^&L4xX3(q6IYPI?xsTDCXRnyk1?2%~2I32mm=34e
+ * 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
@@ -6464,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 6114a2e26..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,8 +19,8 @@
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
*/
@@ -48,8 +48,9 @@ void aParagraphsMarkCarriesWhatItsTextEndsInSoEnterContinuesIt() throws Exceptio
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);
- assertThat(mark.getColorArray(0).getVal()).as("in its colour")
- .isEqualTo(paragraph.getRuns().get(paragraph.getRuns().size() - 1).getCTR().getRPr().getColorArray(0).getVal());
+ 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");
}
}
@@ -96,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();
}
}
From ed9276ed2f77ae752423ab8012c405aad933e987 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 4 Oct 2026 23:31:08 +0100
Subject: [PATCH 07/10] test(docx): take the last letter by position, and
describe the end-of-document check and -Scenario
---
CONTRIBUTING.md | 6 ++++--
scripts/docx-visual/edit-protocol-corpus.ps1 | 4 ++--
2 files changed, 6 insertions(+), 4 deletions(-)
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/scripts/docx-visual/edit-protocol-corpus.ps1 b/scripts/docx-visual/edit-protocol-corpus.ps1
index 1a7e55e8a..2cb785d8f 100644
--- a/scripts/docx-visual/edit-protocol-corpus.ps1
+++ b/scripts/docx-visual/edit-protocol-corpus.ps1
@@ -347,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'
@@ -453,7 +453,7 @@ function Invoke-Protocol {
$last = $doc.Paragraphs($doc.Paragraphs.Count)
if ((Get-Text $last).Length -gt 0) {
$lastLetter = Get-TextRange $last
- $lastLetter.MoveStart(1, $lastLetter.End - $lastLetter.Start - 1) | Out-Null
+ $lastLetter.Start = $lastLetter.End - 1
$state.continues = [double]$lastLetter.Font.Size
}
$end = $doc.Content
From ec2e175591a41d50b9df2dd81b2ef6211ed5933f Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 4 Oct 2026 23:31:08 +0100
Subject: [PATCH 08/10] test(docx): take the Linux LibreOffice baseline from CI
for marks set in their text's style
---
.../docx-fidelity/libreoffice-linux-lines.tsv | 40 +++++++++----------
1 file changed, 20 insertions(+), 20 deletions(-)
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
From e64ea06282e4a5a373d2824e31530ca612a94794 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 4 Oct 2026 23:31:35 +0100
Subject: [PATCH 09/10] docs(docx): record that a paragraph ending in a chip
keeps its own style on its mark
---
CHANGELOG.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d606ca139..29988da3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,7 +15,9 @@ follow semantic versioning; release dates are ISO 8601.
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. Measured with the Word
+ 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 61, against 37;
- text typed at a document's end stands as the page sets its last paragraph in 6 more.
From 420e79e8385810f81d0141015f238886d5e94fa7 Mon Sep 17 00:00:00 2001
From: DemchaAV
Date: Sun, 4 Oct 2026 23:45:54 +0100
Subject: [PATCH 10/10] docs(docx): record Enter continuing the text in all 62
corpus documents
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29988da3e..1d7053f23 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,7 +19,7 @@ follow semantic versioning; release dates are ISO 8601.
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 61, against 37;
+ - 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