Skip to content

fix(docx): a run keeps the style it carries - #523

Merged
DemchaAV merged 3 commits into
developfrom
fix/docx-per-run-styling
Aug 8, 2026
Merged

fix(docx): a run keeps the style it carries#523
DemchaAV merged 3 commits into
developfrom
fix/docx-per-run-styling

Conversation

@DemchaAV

@DemchaAV DemchaAV commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Why

A RichText paragraph exported to DOCX with every run in the paragraph's base style. A bold
segment, an accent-coloured segment and plain text all came out identical — a valid .docx,
no warning, and the emphasis simply absent. From the consumer's side there is nothing to
diagnose: the text is all there, styled as if it had been asked for that way.

The data was never missing. InlineTextRun is (text, textStyle, linkTarget) and its Javadoc
states the style falls back to the paragraph's when null. The backend applied that fallback
unconditionally, inside a loop that already held the run:

node.inlineTextRuns().forEach(run -> {
    XWPFRun docRun = para.createRun();
    applyStyle(docRun, node.textStyle());   // the run's own style, discarded
    docRun.setText(run.text());
});

STRIKETHROUGH was separately the one DocumentTextDecoration with no branch in the style
mapping, falling through default -> {} to no decoration at all.

What changed

  • A run's own style is used, the paragraph's is the fallback — the contract
    InlineTextRun already documents. The fallback replaces the whole style rather than merging
    fields, which is what ParagraphWrapping does for the same case, so DOCX moves toward the
    PDF and PPTX behaviour rather than inventing a third rule.
  • Paragraphs and row cells share one walk, writeParagraphRuns. A row cell was written
    from the concatenated paragraph.text() in a single style; it now goes through the runs.
  • No text moves as a result. That holds because ParagraphNode fills a blank text by
    concatenating exactly the runs inlineTextRuns() returns — and because the DSL keeps text
    and inlineRuns mutually exclusive, so the diverging state cannot be built. The test asserts
    the cell's text as well as its runs.
  • A chip reaches Word with its own face. inlineTextRuns() lowers an InlineHighlightRun
    to a text run carrying the chip's style, so inlineCode("run()") now arrives in Courier
    instead of the paragraph's family. Its background still does not export.
  • STRIKETHROUGH maps to w:strike.

Deliberately out of scope, and the remaining halves of #447: table colSpan/rowSpan and
per-cell styling in writeTable (the TableNode path, still written from lines in no style),
image fitMode/scale, and hyperlink relationships — linkTarget is still dropped on both a
run and a node.

Verification

./mvnw -B -ntp clean verify -pl :graph-compose-core,…,:graph-compose-coverage -am
BUILD SUCCESS, 1563 tests (476 / 144 / 24 / 5 / 90 / 104 / 720), 0 failures, 0 errors.
render-docx goes 18 → 24.

New DocxRunStyleTest, 6 tests: each run keeping its own style against the paragraph's
fallback; a run without a style still taking the paragraph's; STRIKETHROUGH; ITALIC,
UNDERLINE and BOLD_ITALIC per run; a code chip carrying its own face; and a row cell
keeping per-run styling while its text stays byte-identical.

Reverting only DocxSemanticBackend.java to develop turns 3 of the first 4 red
eachRunKeepsItsOwnStyleRatherThanTheParagraphFallback, strikethroughReachesTheDocument and
aTableCellKeepsPerRunStylingAndLosesNoText. aRunWithoutItsOwnStyleTakesTheParagraphStyle
passes in both states by design: it pins the fallback that had to survive the fix, not the bug.

Documentation

docs/architecture/backend-capability-matrix.md — the decorations row moves to ✅ for DOCX; the
paragraph row stays ⚠️, now naming linkTarget as the reason rather than run styling.
render-docx/README.md — the two bullets that described the flattening and the dropped
strikethrough are gone, and the table-cell bullet now distinguishes a table cell (written
from lines, no styling) from a row cell (a paragraph, styled per run).

Lane: shared-engine — a render backend; no public API change.

Part of #447.

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.
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.
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.
@DemchaAV
DemchaAV merged commit f27de45 into develop Aug 8, 2026
12 checks passed
@DemchaAV
DemchaAV deleted the fix/docx-per-run-styling branch August 8, 2026 14:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants