From 80f3cbb070ced12552d673066c80ce4c99d355f8 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sat, 8 Aug 2026 13:08:49 +0100 Subject: [PATCH 1/3] fix(docx): a run keeps the style it carries A RichText paragraph exported with every run in the paragraph's base style, so a bold segment, an accent-coloured segment and plain text came out identical. The file was valid and nothing warned; the emphasis was simply absent. InlineTextRun documents its style as falling back to the paragraph's when null, and the backend applied that fallback unconditionally with the run in hand. It now uses the run's own style and falls back only when there is none, which is the same whole-style fallback the layout engine already does. Paragraphs and row cells share one walk. A row cell used to be written from the concatenated text in a single style; going through the runs changes no text, because a blank text is filled from exactly the runs the walk visits. A chip reaches Word with its own monospace face as a result, though still without its background. A table cell is written from lines rather than runs and is untouched here. STRIKETHROUGH was the one decoration with no branch in the style mapping and fell through to nothing. --- CHANGELOG.md | 14 ++ .../architecture/backend-capability-matrix.md | 4 +- render-docx/README.md | 15 +- .../semantic/docx/DocxSemanticBackend.java | 43 +++-- .../semantic/docx/DocxRunStyleTest.java | 163 ++++++++++++++++++ 5 files changed, 218 insertions(+), 21 deletions(-) create mode 100644 render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxRunStyleTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c6dd112c..bc46e28c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,20 @@ follow semantic versioning; release dates are ISO 8601. other, so it catches one being corrected without the other — not a pair that was wrong together from the start. +### Fixed + +- **DOCX keeps the styling a mixed paragraph asks for.** A `RichText` paragraph exported + with every run in the paragraph's base style, so a bold segment, an accent-coloured + segment and plain text all came out identical — a valid `.docx`, no warning, and the + emphasis simply absent. `InlineTextRun` documents its style as falling back to the + paragraph's *when null*; the backend was applying that fallback unconditionally, with + the run in hand. Each run now carries its own style, in a paragraph and in a `row` + cell, which used to be written from its concatenated text in one style. (A `table` + cell is written from lines rather than runs and still carries no styling.) + +- **`STRIKETHROUGH` reaches Word.** It was the one `DocumentTextDecoration` with no + branch in the DOCX style mapping and fell through to no decoration at all. + ## v2.1.1 — 2026-08-05 ### Build diff --git a/docs/architecture/backend-capability-matrix.md b/docs/architecture/backend-capability-matrix.md index 3008b103..07ef4766 100644 --- a/docs/architecture/backend-capability-matrix.md +++ b/docs/architecture/backend-capability-matrix.md @@ -54,7 +54,7 @@ Payload records live in `core` under | Capability (payload) | PDF (fixed) | PPTX (fixed) | DOCX (semantic) | |---|---|---|---| -| Paragraph — pre-wrapped lines, runs, alignment (`ParagraphFragmentPayload`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` (one absolute, wrap-disabled frame per measured line) | ⚠️ semantic paragraphs (`DocxSemanticBackend`) — every run takes the paragraph's style, so per-run styling and `linkTarget` are dropped | +| Paragraph — pre-wrapped lines, runs, alignment (`ParagraphFragmentPayload`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` (one absolute, wrap-disabled frame per measured line) | ⚠️ semantic paragraphs (`DocxSemanticBackend`) — each run keeps its own style, falling back to the paragraph's when it has none; `linkTarget` is still dropped | | Inline code/badge chips (`InlineBackground` on text spans) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ❌ | | Inline images (`ParagraphImageSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ✅ `PptxParagraphFragmentRenderHandler` | ❌ | | Inline vector shapes (`ParagraphShapeSpan`) | ✅ `PdfParagraphFragmentRenderHandler` | ⚠️ `PptxParagraphFragmentRenderHandler` + `PptxInlineGeometry` (distinct per-corner radii render with the top-left radius — single-adjust preset) | ❌ | @@ -75,7 +75,7 @@ Payload records live in `core` under | Anchor markers (`AnchorMarkerPayload`) | ✅ `PdfAnchorMarkerRenderHandler` + `PdfInternalLinkWriter` | ✅ `PptxAnchorMarkerRenderHandler` + `PptxNavigationWriter` (slide-jump hyperlinks resolved after all fragments, so forward references work) | ❌ | | Bookmark markers (`BookmarkMarkerPayload`) | ✅ `PdfBookmarkMarkerRenderHandler` + `PdfBookmarkOutlineWriter` | ⚠️ `PptxBookmarkMarkerRenderHandler` + `PptxNavigationWriter` (PPTX has no outline tree — the first bookmark on a page names its slide, further bookmarks on the same page are dropped with a debug note) | ❌ | | Alpha / opacity | ✅ `PdfAlphaSupport` (`PDExtendedGraphicsState` on every surface — shape fills/strokes, text runs, lines, side borders, table paint) | ✅ native `` via POI on every surface — fills, strokes, text runs, table paint | ❌ | -| Text decorations — underline / strikethrough (`DocumentTextDecoration`) | ✅ `PdfTextDecorations` (em-proportional marks: underline −0.10 em, strikethrough +0.28 em, thickness 0.05 em) | ✅ `PptxTextFrames.applyStyle` (PowerPoint draws its own marks — sub-point placement differences vs the PDF's constants) | ⚠️ `DocxSemanticBackend.applyStyle` (underline maps to Word's single underline; `STRIKETHROUGH` is dropped) | +| Text decorations — underline / strikethrough (`DocumentTextDecoration`) | ✅ `PdfTextDecorations` (em-proportional marks: underline −0.10 em, strikethrough +0.28 em, thickness 0.05 em) | ✅ `PptxTextFrames.applyStyle` (PowerPoint draws its own marks — sub-point placement differences vs the PDF's constants) | ✅ `DocxSemanticBackend.applyStyle` (underline maps to Word's single underline, strikethrough to `w:strike`) | ## Navigation and interactivity diff --git a/render-docx/README.md b/render-docx/README.md index a98e423b..37b568dc 100644 --- a/render-docx/README.md +++ b/render-docx/README.md @@ -47,14 +47,16 @@ the boundary and without the transform. A document that draws exports its text a not its drawing. What maps: paragraphs, lists, block images, tables, and document metadata (title, author, -subject, keywords). Run styling carries font family, size, colour, bold, italic and -underline. +subject, keywords). Run styling carries font family, size, colour, bold, italic, +underline and strikethrough, per run rather than per paragraph. What maps only in part: - **Table cells keep their text, not their structure.** `colSpan` and `rowSpan` are not applied, so a table with merged cells exports with its columns misaligned. Per-cell - style and fill/border paint are dropped, and cell text carries no run styling. + style and fill/border paint are dropped, and a `table` cell's text carries no styling + at all — it is written from the cell's lines rather than from runs. (A `row` cell is + a paragraph and does keep per-run styling.) - **Image fit is ignored.** The picture is embedded at the node's width and height; `CONTAIN` and `COVER` therefore behave as `STRETCH`, and an image sized only by `scale` falls back to 100 × 100 pt. @@ -65,11 +67,8 @@ before you promise a `.docx` to a reader: - **Hyperlinks are dropped**, external and internal alike; the link text survives as plain text. - **No bookmarks and no navigation outline.** - **No repeating headers or footers**, and no watermark layer. -- **`STRIKETHROUGH` is dropped** — the other decorations map. -- **No barcodes or QR codes**, and no inline images or code/badge chips inside a paragraph. -- **Per-run styling in a mixed-style paragraph is flattened.** Every run in a `RichText` - paragraph is written with the paragraph's style, so a bold or accent-coloured segment - loses its own styling; the text itself is kept. +- **No barcodes or QR codes**, and no inline images or code/badge chips inside a paragraph + — a chip's text is exported with its own styling, but not its background. - **Output is not byte-deterministic**: rendering twice does not produce identical files. Multi-section documents are a separate case: `renderSections` is declared on the 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 a38c12b9..a8a74180 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 @@ -15,6 +15,7 @@ import com.demcha.compose.document.node.DocumentNode; import com.demcha.compose.document.node.ImageNode; import com.demcha.compose.document.node.PageBreakNode; +import com.demcha.compose.document.node.InlineTextRun; import com.demcha.compose.document.node.ParagraphNode; import com.demcha.compose.document.node.RowNode; import com.demcha.compose.document.node.SectionNode; @@ -295,16 +296,35 @@ private void writeShapeContainer(XWPFDocument document, ShapeContainerNode node) private void writeParagraph(XWPFDocument document, ParagraphNode node) { XWPFParagraph para = document.createParagraph(); para.setAlignment(toAlignment(node.align())); - if (!node.inlineTextRuns().isEmpty()) { - node.inlineTextRuns().forEach(run -> { - XWPFRun docRun = para.createRun(); - applyStyle(docRun, node.textStyle()); - docRun.setText(run.text()); - }); - } else { + writeParagraphRuns(para, node); + } + + /** + * Writes {@code node}'s text into {@code para}, one Word run per inline run. + * + *

A run's own style is used and the paragraph's is the fallback, which is the + * contract {@link InlineTextRun} states: its style + * "falls back to the paragraph style when null". Applying the fallback to every run + * regardless is what flattened a bold segment, an accent-coloured segment and plain + * text into one identical face.

+ * + *

Runs win over {@code text} when both are present, matching how a paragraph is + * rendered elsewhere. Nothing is lost by preferring them: when {@code text} is left + * blank {@code ParagraphNode} fills it by concatenating exactly the runs + * {@code inlineTextRuns()} returns, highlight chips included.

+ */ + private void writeParagraphRuns(XWPFParagraph para, ParagraphNode node) { + List runs = node.inlineTextRuns(); + if (runs.isEmpty()) { XWPFRun docRun = para.createRun(); applyStyle(docRun, node.textStyle()); docRun.setText(node.text() == null ? "" : node.text()); + return; + } + for (InlineTextRun run : runs) { + XWPFRun docRun = para.createRun(); + applyStyle(docRun, run.textStyle() == null ? node.textStyle() : run.textStyle()); + docRun.setText(run.text() == null ? "" : run.text()); } } @@ -377,10 +397,9 @@ private void writeRow(XWPFDocument document, RowNode node) { private void writeRowCellChild(XWPFTableCell cell, DocumentNode child) { if (child instanceof ParagraphNode paragraph) { - XWPFParagraph para = cell.addParagraph(); - XWPFRun run = para.createRun(); - applyStyle(run, paragraph.textStyle()); - run.setText(paragraph.text() == null ? "" : paragraph.text()); + // Same walk as writeParagraph: a cell paragraph keeps per-run styling + // instead of being flattened into the concatenated text in one style. + writeParagraphRuns(cell.addParagraph(), paragraph); } else if (child instanceof SpacerNode) { cell.addParagraph(); } else { @@ -426,6 +445,8 @@ private void applyStyle(XWPFRun run, DocumentTextStyle style) { } case UNDERLINE -> run.setUnderline(org.apache.poi.xwpf.usermodel.UnderlinePatterns.SINGLE); + case STRIKETHROUGH -> run.setStrike(true); + // DEFAULT carries no face of its own and is the only one left. default -> { } } diff --git a/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxRunStyleTest.java b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxRunStyleTest.java new file mode 100644 index 00000000..df0a93a0 --- /dev/null +++ b/render-docx/src/test/java/com/demcha/compose/document/backend/semantic/docx/DocxRunStyleTest.java @@ -0,0 +1,163 @@ +package com.demcha.compose.document.backend.semantic.docx; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.style.DocumentColor; +import com.demcha.compose.document.style.DocumentInsets; +import com.demcha.compose.document.style.DocumentTextDecoration; +import com.demcha.compose.document.style.DocumentTextStyle; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.apache.poi.xwpf.usermodel.XWPFParagraph; +import org.apache.poi.xwpf.usermodel.XWPFRun; +import org.apache.poi.xwpf.usermodel.XWPFTableCell; +import org.apache.poi.xwpf.usermodel.UnderlinePatterns; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.util.List; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Per-run styling in the DOCX semantic backend. + * + *

An {@code InlineTextRun} carries its own style and falls back to the paragraph's + * only when it has none. Applying the paragraph style to every run instead produced a + * valid {@code .docx} in which a bold segment, an accent-coloured segment and plain text + * were indistinguishable — no error, no warning, just a document missing the emphasis it + * was told to carry. These pin the run's own style reaching the file, in a paragraph and + * inside a table cell.

+ */ +class DocxRunStyleTest { + + private static final DocumentTextStyle BASE = DocumentTextStyle.builder().size(11).build(); + private static final DocumentTextStyle BOLD = DocumentTextStyle.builder() + .size(11).decoration(DocumentTextDecoration.BOLD).build(); + private static final DocumentTextStyle ACCENT = DocumentTextStyle.builder() + .size(11).color(DocumentColor.rgb(192, 57, 43)).build(); + + @Test + void eachRunKeepsItsOwnStyleRatherThanTheParagraphFallback() throws Exception { + List runs = paragraphRuns(flow -> flow.addParagraph(paragraph -> paragraph + .textStyle(BASE) + .inlineText("plain ") + .inlineText("bold ", BOLD) + .inlineText("accent", ACCENT))); + + assertThat(runs).hasSize(3); + assertThat(runs.get(0).getText(0)).isEqualTo("plain "); + assertThat(runs.get(0).isBold()).isFalse(); + + assertThat(runs.get(1).getText(0)).isEqualTo("bold "); + assertThat(runs.get(1).isBold()).isTrue(); + + assertThat(runs.get(2).getText(0)).isEqualTo("accent"); + assertThat(runs.get(2).isBold()).isFalse(); + assertThat(runs.get(2).getColor()).isEqualToIgnoringCase("C0392B"); + } + + @Test + void aRunWithoutItsOwnStyleTakesTheParagraphStyle() throws Exception { + // The fallback InlineTextRun documents — it must survive the fix that stopped + // applying it unconditionally. + List runs = paragraphRuns(flow -> flow.addParagraph(paragraph -> paragraph + .textStyle(BOLD) + .inlineText("inherits"))); + + assertThat(runs).hasSize(1); + assertThat(runs.get(0).isBold()).isTrue(); + } + + @Test + void strikethroughReachesTheDocument() throws Exception { + List runs = paragraphRuns(flow -> flow.addParagraph(paragraph -> paragraph + .textStyle(BASE) + .inlineText("struck", DocumentTextStyle.builder() + .size(11).decoration(DocumentTextDecoration.STRIKETHROUGH).build()))); + + assertThat(runs).hasSize(1); + assertThat(runs.get(0).isStrikeThrough()).isTrue(); + } + + @Test + void theRemainingDecorationsAlsoTravelPerRun() throws Exception { + List runs = paragraphRuns(flow -> flow.addParagraph(paragraph -> paragraph + .textStyle(BASE) + .inlineText("i", decorated(DocumentTextDecoration.ITALIC)) + .inlineText("u", decorated(DocumentTextDecoration.UNDERLINE)) + .inlineText("bi", decorated(DocumentTextDecoration.BOLD_ITALIC)))); + + assertThat(runs).hasSize(3); + assertThat(runs.get(0).isItalic()).isTrue(); + assertThat(runs.get(0).isBold()).isFalse(); + assertThat(runs.get(1).getUnderline()).isEqualTo(UnderlinePatterns.SINGLE); + assertThat(runs.get(2).isBold()).isTrue(); + assertThat(runs.get(2).isItalic()).isTrue(); + } + + @Test + void aCodeChipCarriesItsOwnGlyphStyleRatherThanTheParagraphs() throws Exception { + // A chip is lowered to a text run holding the chip's style, so per-run styling + // means the chip's monospace face now reaches Word. Its background does not. + List runs = paragraphRuns(flow -> flow.addParagraph(paragraph -> paragraph + .textStyle(BASE) + .inlineText("call ") + .inlineCode("run()"))); + + assertThat(runs).hasSize(2); + assertThat(runs.get(0).getFontFamily()).isEqualTo("Helvetica"); + assertThat(runs.get(1).getText(0)).isEqualTo("run()"); + assertThat(runs.get(1).getFontFamily()).isEqualTo("Courier"); + } + + @Test + void aTableCellKeepsPerRunStylingAndLosesNoText() throws Exception { + byte[] docx = export(flow -> flow.addRow(row -> row + .addParagraph(paragraph -> paragraph + .textStyle(BASE) + .inlineText("plain ") + .inlineText("bold", BOLD)))); + + try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(docx))) { + assertThat(document.getTables()).hasSize(1); + XWPFTableCell cell = document.getTables().get(0).getRow(0).getCell(0); + List runs = cell.getParagraphs().get(0).getRuns(); + + assertThat(runs).hasSize(2); + assertThat(runs.get(0).isBold()).isFalse(); + assertThat(runs.get(1).isBold()).isTrue(); + // The cell used to be written from the concatenated text in one style. + // Splitting it into runs must not change what the cell says. + assertThat(cell.getText()).isEqualTo("plain bold"); + } + } + + private static DocumentTextStyle decorated(DocumentTextDecoration decoration) { + return DocumentTextStyle.builder().size(11).decoration(decoration).build(); + } + + private static List paragraphRuns(Consumer spec) + throws Exception { + try (XWPFDocument document = new XWPFDocument(new ByteArrayInputStream(export(spec)))) { + List paragraphs = document.getParagraphs().stream() + .filter(paragraph -> !paragraph.getRuns().isEmpty()) + .toList(); + assertThat(paragraphs).hasSize(1); + return paragraphs.get(0).getRuns(); + } + } + + private static byte[] export(Consumer spec) + throws Exception { + try (DocumentSession session = GraphCompose.document() + .pageSize(595, 842) + .margin(DocumentInsets.of(36)) + .create()) { + com.demcha.compose.document.dsl.PageFlowBuilder flow = session.dsl().pageFlow().name("Flow"); + spec.accept(flow); + flow.build(); + return session.export(new DocxSemanticBackend()); + } + } +} From dd9b85d9d27b6e2480fabc0d6ce215e9b6c29984 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sat, 8 Aug 2026 13:40:25 +0100 Subject: [PATCH 2/3] chore(examples): re-render the Word companion preview The committed preview was rendered before runs kept their own style, so it shows the flattening: bold and italic segments carry no run properties and two accent colours are written as black. The catalogue renders it with them now, and the drift guard fails on any preview that no longer matches its example. --- .../examples/word-export-companion.docx | Bin 7952 -> 7969 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/readme/examples/word-export-companion.docx b/assets/readme/examples/word-export-companion.docx index 6fc1905d669bce6761b813277ef95aa15a1eada9..2169cd98a34d876fd5c9286a76500184aec3b978 100644 GIT binary patch delta 2176 zcmYk7c{~%08^Fh~VHk~^!;D3aB#F7tnB-2jy*Z-sO75_jh2)qpqZg4fa)o$9b416? z^&v-IawX)*P4&8dFa7cB`R6%4&-eM~`Qw|WU#QP#ive=L0AMf}Fz*Mh;k(WT{OSj; z4Pg^KE~pRKdyfWGHz1PKW%K3?)QsfyB@~OQG=$RK?cfs7i!kzt7?EIdVr&Cav`<{F zi%Uprs@D6S-x!^FW9%ha0OE*7pZtkD5#x+@7#qUrDyuP3?DuJ!Wq~5|9tp_aGW%U) zM$GND2nbHj=!=U|T|u&Dty&SX`Mq;LQpr!bPFi=rH@Iz}tF-TOUrcFeJ+Kp=58$Nq4>p*jhX4FWJv<<~;EqFJpDs;{c^lV2- zN8`pq!YEIa1il)B98%E>b= zVGMtAn~#u;P=R#o_t=f~&4{cF#gFD4O%msJ7TKn?sWlzU@u`wcqi<)zCw6yoXRe~M z{4&-*35}%Z_EtzBhs$h4N#a>+DLHzj;rzjJr5e74<;-d8MHe4dgF4_0=8bpr1iyyT z#$%4B`kXcu3x1rhL#s=oU&K8#;Cw7e;g~7mw1k(XVq*~`f8iAbIOX;H01eE*{%CF6TmA4 zY3W&F>DnlIQy_I4t5=EcNuP7)v{+nOOffS$T5=2pCSj@S$30wxDQxD7)1#_5dm&28nDTXd+n?Bcwyo0_& zFXsBBGF1PHJr+(pbU$GO>6s#{y3P^5gy!=EC`D#S1pQopFn;o3gM-_UB*4s?llUSx zZX0W1QF(yspWayyx8U}v+gaL;r6>rqV0%DMXiK-vhZ+jWr?By1Vt$e$3V}+mV@>Gs zc7Ed#IhWhBGcadvm*05mPFo{FZ0KZKsm{FGM)*ztjl+Qv9A-wzQuAz#H5J^YVWN;0 zRmiX=_ug&awGviHyWY*F*exwQ6q2H?V249{tfKJQ(uWGS8rnw8QRmaeeJMmsmefi- z?qFL$yMSc*x!5r5X?sK7Cy(cTcl|-}yH4qRafd(E7A~j6$I(Mn=mloUTZ%nz*6ZdU zS^{~M@z89iT&vNWnIcslo|!G6#~NVY`$hOuZs%N*wm|J$ytYzq&zCHn5HWOO=RV>K zZs@{WolKi|DIzmPLCFB~_j^O*4hEC?JXKG{p4*e=3zlCMk4-!{!L#5`E9dW;t=DRv zb;!T`BP{oxYpN+tT!h#zpK-fzbY$hVb1-9@qPe6cS3m2%yNgBKUX$<88(*p3$;$J8 zezjGsaX2hNYOvn)d9z?+zye>3Rl*PcCNmY+!M;DCDmU(#=2<@Tm2ECHI_$?dN3zyl zy2cBTtb_l!5dta9ydmgK5gFhnH^MNAgTrs{3pg3M4ILy&+#)EY-DjW^3ym+_{x`O= zzH2zyYYBq&=Pkf?PmM_0>HB$BT8^+U&d;@NB$-Up4xsj%BWxW$TuN7Q#V6uA_y)!e zWmFo-VK^N%m%x4-gPFMr{C-u84m~R)QKW#xX2tD|61Q*kB!mtMsSu*n>+a~lAn}P( zJKm90j7jUOB+M*-YXL&%Mh0Fqqbu5YQ{6y4Nn?z!B0s$LRf}9xXmlozCe$aXuliC5 zQ_)$guWF_^JEbNEo8=bFfthUQ21j}!n)rE$<`J-YxkuC3RP*aA&u~)%Y+Pop{3>V}PEVt7Jy+1+Ig>OIWVoOcNby6lDEtD{Uj7i@u^>l9Cb z8}bV%6|=PoUUCq!3p1)vek2gEl|OXWNxFXY+YZZ!I3Rp;RdTU@dLf38?^zeL{RsvW z0;pf_*C1U@x2?n2-x;(Ue298W>L_1Y!oByBB7iICadurydtQH>_v@P3&6q`WBPb7q zo6XSjTdgsUtVn~OE_xJLdkyM;CM&r718NW6o`jPPim_^vtwvz>4WHW%s~9LdEkb+q?CEV%8{ delta 2173 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 Q20l&sfk3kV6#rfK7ZsxC!vFvP From d2afc488fc187a09a03114203aa82e989a803fc2 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sat, 8 Aug 2026 15:02:21 +0100 Subject: [PATCH 3/3] fix(docx): write strikethrough through the API POI still supports setStrike(boolean) is deprecated in POI 5.5.1; setStrikeThrough(boolean) is the replacement and sets the same single strike. javac reports the deprecation under -Dmaven.compiler.showDeprecation and stops once the call moves. The export is unchanged: the run-style suite and the committed-asset guard both stay green. --- .../document/backend/semantic/docx/DocxSemanticBackend.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a8a74180..9139e22d 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 @@ -445,7 +445,7 @@ private void applyStyle(XWPFRun run, DocumentTextStyle style) { } case UNDERLINE -> run.setUnderline(org.apache.poi.xwpf.usermodel.UnderlinePatterns.SINGLE); - case STRIKETHROUGH -> run.setStrike(true); + case STRIKETHROUGH -> run.setStrikeThrough(true); // DEFAULT carries no face of its own and is the only one left. default -> { }