Skip to content

fix(layout): paint the Word 97-2003 run effects and the double strike - #3983

Open
Nathaniel-260 wants to merge 1 commit into
superdoc:mainfrom
Nathaniel-260:fix/legacy-run-effects
Open

fix(layout): paint the Word 97-2003 run effects and the double strike#3983
Nathaniel-260 wants to merge 1 commit into
superdoc:mainfrom
Nathaniel-260:fix/legacy-run-effects

Conversation

@Nathaniel-260

@Nathaniel-260 Nathaniel-260 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

The bug

The Font dialog's Effects group in Word writes four run flags โ€” w:outline,
w:shadow, w:emboss, w:imprint โ€” and Word still honours all four. They
predate the w14: effect family, so a document can carry them with no w14:
counterpart anywhere.

SuperDoc imports them, keeps them, and exports them correctly. It never paints
them. Applying one to a selection writes the file and leaves the page identical.

w:dstrike is the same seam from the other side. normalizeRunAttrsFromOoxml
already emits doubleStrike and it reaches the painter โ€” the painter simply
never reads it, so a double strikethrough is drawn as a single line.

Reproduce

  1. Open any .docx in SuperDoc.
  2. Select a word and apply format.apply({ target, inline: { outline: true } })
    (or shadow / emboss / imprint / dstrike).
  3. Export the document: the flag is there, canonical.
  4. Look at the page: nothing changed. For dstrike, a single line.

Or in Word: type a word, Font โ†’ Effects โ†’ Outline, save, open in SuperDoc.

Measurement

Chrome, packaged dist, one effect per run so each pixel is attributable, with
getComputedStyle on the .superdoc-text-run element and a byte-compare of
a PNG clip of the line before and after:

applied in the exported document.xml painted, before painted, with this patch
outline <w:outline/> nothing โ€” PNG identical, 768/768 bytes hairline stroke, fill emptied
shadow <w:shadow/> nothing โ€” PNG identical text-shadow, one offset copy
emboss <w:emboss/> nothing โ€” PNG identical light edge up-left, dark down-right
imprint <w:imprint/> nothing โ€” PNG identical the mirror of emboss
dstrike <w:dstrike/> line-through solid line-through double

The four flags never reached paint at all. Dumping the keys of the TextRun the
painter receives, on a run carrying each flag, gave:

outline / shadow / emboss / imprint โ†’  kind|text|fontFamily|fontSize|color|script|pmStart|pmEnd
dstrike                             โ†’  kind|text|fontFamily|fontSize|strike|doubleStrike|color|script|pmStart|pmEnd

So dstrike was a painter-only gap, and the other four were dropped one layer
earlier, in the normalizer. Both halves are in this patch, and the same dump with
the patch applied shows outline / shadow / emboss / imprint on the run
alongside doubleStrike.

The change

style-engine/normalize/run-attrs.ts passes the four flags through as
authored, including an explicit false โ€” <w:shadow w:val="0"/> is how Word
clears an inherited effect, and dropping the key would let a lower cascade layer
switch it back on. This mirrors the vanish / specVanish lines directly below.

contracts carries them on RunMarks, next to strike, so every run kind
that supports inline formatting has them.

painters/dom/runs/text-run.ts draws them in a small applyLegacyRunEffects,
called after applyTextEffects so it can see what an authored w14: effect
already wrote:

  • it stands aside when the run carries a w14:textOutline that actually
    painted a stroke
    , or a w14:textFill, rather than relying on declaration
    order โ€” order would have left its empty fill behind on a run whose
    w14:textOutline has no fill of its own, turning an authored outline into a
    hollow glyph. The test is what was written, not whether the object exists:
    <w14:textOutline w14:w="0"><w14:noFill/></w14:textOutline> โ€” what Word
    writes for "no outline" โ€” is a present object that paints nothing, and
    standing aside for it would leave the run blank;
  • it stands aside for a w14:shadow in the same way, since that is the authored
    form of the same drop shadow โ€” while emboss and imprint, which have no w14:
    counterpart and are not drop shadows, keep composing;
  • and it appends its shading to any existing text-shadow instead of
    assigning over it, because w14:glow is a different effect from an embossed
    edge and the two compose as two shadow layers โ€” assigning would let a glow
    silently erase an emboss on the same run.

The outline empties the glyph through -webkit-text-fill-color, not color.
The stroke is currentColor, and currentColor resolves against the element's
own color; clearing color takes the stroke down with the fill, and a run
carrying <w:outline/> with Automatic color โ€” exactly what the Font dialog
writes โ€” would paint nothing at all. normalizeRunAttrsFromOoxml deliberately
reports Automatic as no color, so that is the default path, not an edge case.

doubleStrike draws its own line. It is a separate RunMarks field and a
separately settable run attribute, so a run carrying only w:dstrike still has a
strikethrough; reading the line off strike alone would render it undecorated.
render-line.ts re-derives the decoration on the two paths that lift an underline
onto a line overlay, so that rule now lives in one shared helper rather than in
three places that had already drifted.

The outline also fixes the formatting marks it would otherwise empty.
-webkit-text-fill-color is inherited and beats a child's own color, so the
ยท and โ†’ marks rendered inside an outlined run would have come out as hairline
rings. Both ::after rules opt back out โ€” they are chrome, not document text.

Five signatures gate paint reuse, and all five count the new flags โ€”
textRunMergeSignature, hashRunVisualMarks, deriveBlockVersion,
deriveParagraphBlockVersion and hashParagraphBlockForTableVersion. Without them an edit that flips only <w:emboss/>
leaves the already-painted span on screen โ€” the same "the file changed and the
page did not" symptom, one layer up โ€” and two adjacent runs differing only in one
flag merge into a single span, painting one run's effect over the other's text.

On fidelity, stated rather than hidden

  • outline is faithful โ€” a hairline stroke with the fill removed is what Word
    draws. Stroke weight scales with font size, with a floor so it does not round
    away on a low-density screen.
  • shadow is a hard-edged offset copy, as in Word. The blur Word applies at
    large display sizes is not reproduced.
  • emboss / imprint keep the authored glyph color and add a light edge on one
    side and a dark edge on the other; the direction is what separates the two.
    Word instead paints the glyph itself near the page color and lets the edges
    carry the shape. Doing that in paint needs the resolved page background, which
    the painter does not have โ€” and guessing it wrong turns the text invisible.
    That is the one place this trades the last of the fidelity for legibility, and
    the reasoning is in the source rather than only here.

This is the same trade applyTextEffects already makes for w14:glow, which it
approximates with concentric zero-offset shadows.

One CSS constraint

text-decoration-style is a single value for the whole decoration set, so a run
that is both underlined and double-struck can keep only one of the two styles.
The underline already claimed it, and it claimed it from an authored value
(w:u/@w:val) while w:dstrike has no style to lose โ€” so the underline keeps
its style and the strike stays single. A visible shortfall on a rare combination,
rather than a wrong underline on a common one.

Deliberately not in scope

w:kern is measured as written-but-not-drawn in the same dialog โ€” the painter
sets font-kerning: none unconditionally and nothing opts a run back in. It is
left out on purpose: kerning changes glyph advance widths, so it is a
measurement change, not a paint-only one, and it belongs with the layout path
rather than in this patch.

Tests

Added, and each was watched fail before it passed โ€” the fix was disabled in place
and the suites re-run:

  • painters/dom, 18 new cases: the hollow outline; an outlined run with no
    explicit color
    staying visible (the case that goes blank if the fill is
    cleared through color); the single offset shadow; emboss vs imprint differing
    by side; both scaling with font size; standing aside for w14:textOutline;
    standing aside for w14:textFill; standing aside for w14:shadow; composing with w14:glow; the double strike;
    a run carrying only doubleStrike; the underline keeping its style against
    a double strike; no effect painting nothing; and the merge signature changing
    for each of the five flags. With the painter pass disabled, 5 go red; with the
    fill cleared through color again, 2 go red; with the ordering reverted, 1 goes
    red; with doubleStrike dropped from the decoration test, 1 goes red.
  • layout-bridge, 2 new cases on hashRunVisualMarks: a different hash for
    each flag, and the flags distinguished from one another. Both go red when the
    five lines are removed from the hash.
  • style-engine, 3 new cases: the flags carried through, an explicit false
    preserved, and absence staying absent. With the four normalizer lines removed,
    2 go red.
packages/layout-engine/painters/dom + layout-bridge + layout-resolved + contracts
                                       4215 passed  (190 files)
packages/layout-engine/style-engine    181 passed   (bun test, whole package)
tsc -b tsconfig.references.json        clean
vp lint / vp fmt --check               no new findings

A note on how this was reviewed: three independent review passes were run over
the change across two rounds. They found eleven real defects, including two that
an earlier revision of this patch introduced โ€” the invisible default-color
outline and the missed invalidation signatures. All are fixed here, each with a
test that was watched go red first.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/layout-engine/painters/dom/src/runs/text-run.ts Outdated
Comment thread packages/layout-engine/painters/dom/src/runs/text-run.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/layout-engine/painters/dom/src/runs/text-run.ts
Nathaniel-260 added a commit to Nathaniel-260/otzaria-word-editor that referenced this pull request Sep 7, 2026
ื“ื•ื•ื—: โ€žื›ืคืชื•ืจ ืžืชืงื“ื ืคื•ืชื— ืคืืค, ืื ื™ ื‘ื•ื—ืจ ืขื™ืฆื•ื‘, ืœื•ื—ืฅ, ื•ื–ื” ืœื ืขื•ืฉื” ื›ืœื•ืโ€.
ื ืžื“ื“ ื‘-Chrome ืืžื™ืชื™ ืขืœ ื”-dist ื”ืืจื•ื– โ€” ืคื™ืจื•ืง ื”-zip ืฉืœ `export.toDocx` ืžื•ืœ
`getComputedStyle` ืขืœ `.superdoc-text-run`, ื•ื‘ื”ืฉื•ื•ืืช ืฆื™ืœื•ื ืžืกืš ื‘ื™ื™ื˜-ื‘ื‘ื™ื™ื˜
ืฉืœ ื”ืฉื•ืจื” ืœืคื ื™ ื•ืื—ืจื™ โ€” ื•ื ืžืฆืื• ืฉื ื™ ื“ื‘ืจื™ื ืฉื•ื ื™ื ืœื’ืžืจื™ ืฉื ืจืื™ื ืœืžืฉืชืžืฉ ืื•ืชื• ื“ื‘ืจ.

**ื”ืื—ื“: ืื™ืŸ ื˜ืงืกื˜ ืžืกื•ืžืŸ.** `format.apply` ืžืงื‘ืœ SelectionTarget ื‘ืœื‘ื“, ื•ืขื“
ืขื›ืฉื™ื• ื”ืชืฉื•ื‘ื” ื”ื’ื™ืขื” ืจืง ื‘ืจื’ืข ื”โ€žืื™ืฉื•ืจโ€: ื”ื“ื™ืืœื•ื’ ื ืกื’ืจ, ืœืงื— ืื™ืชื• ืืช ืฉื‘ืขื”-ืขืฉืจ
ื”ืฉื“ื•ืช ืฉืžื•ืœืื•, ื•ื”ืฉืื™ืจ โ€žื™ืฉ ืœืกืžืŸ ื˜ืงืกื˜ ืชื—ื™ืœื”โ€ ื‘ืฉื•ืจืช ื”ืžืฆื‘. ื–ื• ืื™ื ื” ืžื’ื‘ืœืช ืžื ื•ืข
ืืœื ืกื“ืจ ืฉืœ ืžืžืฉืง, ื•ื”ื™ื ืชื•ืงื ื” ื‘ืฉืœื•ืฉื” ื—ืœืงื™ื:

1. `hasRangeSelection` (engine/font-advanced.ts) ื”ื™ื ืื•ืชื” ืงืจื™ืื” ื‘ื“ื™ื•ืง ืฉืœ
   `applyFontAdvanced` โ€” ืœื ืฉืืœื” ืฉื ื™ื™ื” ืฉืขืœื•ืœื” ืœืขื ื•ืช ืื—ืจืช โ€” ื•ื”ื™ื ื ืฉืืœืช
   ื‘ืคืชื™ื—ื”. ืืจื‘ืข ืชืฉื•ื‘ื•ืช ื•ืœื ืฉืชื™ื™ื: โ€žื™ืฉ ื˜ื•ื•ื—โ€, โ€žืื™ืŸ ื‘ื—ื™ืจื”โ€, โ€žืื™ืŸ
   `format.apply` ื‘ื’ืจืกื” ื”ื–ืืชโ€, ื•โ€žืื™ืŸ ืœื“ืขืชโ€ ืฉืื™ื ื• ื ื•ืขืœ ื“ื‘ืจ. โ€žืื™ืŸ ืžืกืžืšโ€
   ื ื›ืœืœ ื‘ืื—ืจื•ืŸ: ื‘ื™ืŸ ืžืกืžื›ื™ื ื•ื‘ื–ืžืŸ ื˜ืขื™ื ื” ื”ืžืกืžืš ื”ื•ื `null`, ื•ื”ื•ื“ืขื” ืฉืื•ืžืจืช ืฉื
   โ€žืื™ื ื• ื–ืžื™ืŸ ื‘ื’ืจืกื” ื–ื• ืฉืœ ื”ืžื ื•ืขโ€ ื”ื™ื ื˜ืขื ื” ืฉืงืจื™ืช ืขืœ ื”ื‘ื ื™ื™ืŸ.
2. **ื•ื”ื™ื ื ืฉืืœืช ืฉื•ื‘.** ื”ื“ื™ืืœื•ื’ ืื™ื ื• ื—ื•ืกื ืืช ื”ืžืกืžืš โ€” ืื™ืŸ ืžืื—ื•ืจื™ื• ืจืงืข, ื”ื•ื
   ื ื’ืจืจ ื‘ื›ื•ื•ื ื”, ื•ืืคืฉืจ ืœืœื—ื•ืฅ ื‘ื˜ืงืกื˜ ื‘ื–ืžืŸ ืฉื”ื•ื ืคืชื•ื—. ืชืฉื•ื‘ื” ืฉื ืงืจืื” ืคืขื ืื—ืช
   ืžืชื™ื™ืฉื ืช ื‘ืฉื ื™ ื”ื›ื™ื•ื•ื ื™ื, ื•ืฉื ื™ื”ื ืžื—ื–ื™ืจื™ื ืืช ื”ื‘ืื’: ื‘ื—ื™ืจื” ืฉื”ืชื›ื•ื•ืฆื” ืžืชื—ืช
   ืœื›ืคืชื•ืจ ืฉื ืฉืืจ ืคืชื•ื—, ืื• ื›ืคืชื•ืจ ืฉื ืขื•ืœ ืœื ืฆื— ืื—ืจื™ ืฉื”ืžืฉืชืžืฉ ืกื™ืžืŸ ื˜ืงืกื˜ ื›ืžื•
   ืฉื”ื”ื•ื“ืขื” ื‘ื™ืงืฉื”. ืžืื–ื™ืŸ `selectionchange` ืžื›ืกื” ืืช ืฉื™ื ื•ื™ื™ ื”ื‘ื—ื™ืจื”, ื•-`watch`
   ืขืœ ื”ืžืกืžืš ื”ืคืขื™ืœ ืžื›ืกื” ืžืขื‘ืจ ื‘ื™ืŸ ืœืฉื•ื ื™ื•ืช โ€” ืฉืื™ื ื• ืžื™ื™ืฆืจ `selectionchange`.
   ืคืก ื”ืชืฆื•ื’ื” ื”ืžืงื“ื™ืžื” ื ืงืจื ืžื—ื“ืฉ ื™ื—ื“ ืขื ื”ืชืฉื•ื‘ื”.
3. **ื•ื”ื“ื™ืืœื•ื’ ื ืกื’ืจ ืขืœ ื”ืฆืœื—ื” ื‘ืœื‘ื“.** ืงื•ื“ื ื”ื•ื ื ืกื’ืจ ืœืคื ื™ ืฉื”ื”ื—ืœื” ื™ืฆืื” ืœื“ืจืš.

**ื•ื”ืฉื ื™: ืคืงื“ื™ื ืฉื ื›ืชื‘ื™ื ื•ืื™ื ื ืžืฆื•ื™ืจื™ื.** ืžืกื’ืจืช ืœืชื•, ืฆืœ, ื—ืจื•ื˜ ื•ืฉืงื•ืข ื™ื•ืฆืื™ื
ืœ-docx ืงื ื•ื ื™ืช ื•ืžื•ืฆื’ื™ื ื‘-Word, ื•ื”ืขื•ืจืš ืื™ื ื• ืžืฆื™ื™ืจ ืืฃ ืื—ื“ ืžื”ื โ€” ืฆื™ืœื•ื ื”ืžืกืš
ื–ื”ื” ื‘ื™ื™ื˜-ื‘ื‘ื™ื™ื˜. โ€žืงื• ื—ื•ืฆื” ื›ืคื•ืœโ€ ืžืฆื•ื™ืจ, ืื‘ืœ ื›ืงื• ื‘ื•ื“ื“. ื•ื‘ืื•ืชื” ืจืฉืช ืคืงื“ื™ื ื’ื
โ€žืงืจื ื™ื ื’โ€ ื•โ€žื’ื•ื“ืœ ื”ื’ื•ืคืŸ ื”ืžื•ืจื›ื‘โ€ ืื™ื ื ืžืฆื•ื™ืจื™ื, ื•ื›ืš ื›ืœ ืžื—ืกื ื™ืช ื”ื›ืชื‘ ื”ืžื•ืจื›ื‘.
ืžื‘ื—ื™ื ืช ืžื™ ืฉืœื—ืฅ โ€žืื™ืฉื•ืจโ€ ื–ื” ื‘ื“ื™ื•ืง ื›ืžื• ืคืงื“ ืฉืื™ื ื• ืขื•ื‘ื“ โ€” ื•ืžื›ื™ื•ื•ืŸ ืฉื”ืคืขื•ืœื”
ืžื“ื•ื•ื—ืช ื”ืฆืœื—ื”, ื’ื ืฉื•ืจืช ื”ืžืฆื‘ ื ืฉืืจืช ืจื™ืงื”.

ืื™ืŸ ืžื” ืœืขืฉื•ืช ื‘ืฆื“ ืฉืœื ื•: `.superdoc-text-run` ืื™ื ื• ื ื•ืฉื ืฉื•ื ืกื™ืžืŸ ืœืžืืคื™ื™ื ื™
ื”ืจื™ืฆื”, ื•ืœื›ืŸ ืื™ืŸ ื•ื• ืœ-CSS ืžืงื•ืžื™, ื•ื”ืฉืœื‘ ื”ืžืงื“ื™ื (docx-preflight) ืจืฅ ื‘ืคืชื™ื—ื”
ื‘ืœื‘ื“ ื•ืœื ื”ื™ื” ื ื•ื’ืข ื‘ืขื™ืฆื•ื‘ ืฉืžื•ื—ืœ ืขื›ืฉื™ื• โ€” ืฉื–ื” ื‘ื“ื™ื•ืง ื”ืžืงืจื” ืฉื“ื•ื•ื—.

ืžื” ืฉื›ืŸ: ื”ื“ื™ืืœื•ื’ ืื•ืžืจ ืืช ื–ื”, ืžืชื—ืช ืœืคืก ื”ืชืฆื•ื’ื” ื”ืžืงื“ื™ืžื”, ื•ืจืง ืขืœ ืžื” ืฉื ื‘ื—ืจ.
ื”ื›ื™ืชื•ื‘ ืฉืœ ื”ืคืก ื”ืคืš ืœโ€žื›ืš ื™ื™ืจืื” ื‘-Word (ืงื™ืจื•ื‘)โ€ โ€” ื”ื•ื ืžืชื—ื™ื™ื‘ ืขืœ ื”ืงื•ื‘ืฅ,
ื•ื”ื”ื•ื“ืขื” ืฉืžืชื—ืชื™ื• ืื•ืžืจืช ืžื” ื”ืžืกืš ืœื ื™ืจืื”. ื”ื•ื“ืขื” ื•ืœื ืื–ื”ืจื”: ืฉื•ื ื“ื‘ืจ ืœื ื ื›ืฉืœ.

**ื”ืชื™ืงื•ืŸ ืขืฆืžื• ื ืฉืœื— ืœืžืขืœื”:** superdoc/docx-editor#3983. ืฉืชื™ ื”ืฉื›ื‘ื•ืช ืฉื‘ื•ืœืขื•ืช
ืื•ืชืจื• ื‘ืžื“ื™ื“ื” โ€” `dstrike` ื”ื•ื ืคืขืจ ืฉืœ ื”ืฆื™ื™ืจ ื‘ืœื‘ื“, ื•ืืจื‘ืขืช ื”ืื—ืจื™ื ื ื‘ืœืขื™ื
ื‘-`normalizeRunAttrsFromOoxml`. ื”ื’ืฉืจ ืืœ ื—ื•ื–ื” ื”ืคืจื™ืกื” ื’ื ืจื™, ื•ืœื›ืŸ ื›ืœ ื”ืชื™ืงื•ืŸ
ื™ื•ืฉื‘ ื‘ืงื•ื“ ื”-OSS. `w:kern` ื”ื•ืฉืืจ ื‘ื—ื•ืฅ ื‘ืžืคื•ืจืฉ: ืงืจื ื™ื ื’ ืžืฉื ื” ืืช ืจื•ื—ื‘ ื”ื–ื—ื™ืœื”
ืฉืœ ื”ื’ืœื™ืคื™ื, ื›ืœื•ืžืจ ืืช ื”ืžื“ื™ื“ื” ื•ืœื ืจืง ืืช ื”ืฆื™ื•ืจ. ื”ืจื™ืฉื•ื ื”ืžืœื ื‘-docs/engine-gaps.md.

**ืฉืขืจ ื—ื“ืฉ:** `npm run check:font-advanced` โ€” 5/5 ื‘-Chrome ืืžื™ืชื™. ืฉืœื•ืฉ
ืžืฉื•ืจื•ืชื™ื• ืžื•ื“ื“ื•ืช ืืช ื”ื”ื•ื“ืขื” ืขืฆืžื” ืžื•ืœ ื”ืคื™ืงืกืœื™ื, ืคืงื“ ืื—ื“ ืœื›ืœ ืจื™ืฆื” ื›ื“ื™ ืฉืืคืฉืจ
ื™ื”ื™ื” ืœื™ื™ื—ืก ืคื™ืงืกืœ ืœืคืงื“: ื‘ืจื’ืข ืฉื”ืžื ื•ืข ื™ืชื—ื™ืœ ืœืฆื™ื™ืจ ืฆืœ, ืื• ืงื• ื›ืคื•ืœ, ื”ืฉื•ืจื”
ืชื™ืคื•ืœ ื•ืชืืžืจ ืžื” ืœื”ื•ืจื™ื“ ืžื”ื”ื•ื“ืขื”. ื”ื•ื“ืขื” ืฉืื™ืฉ ืื™ื ื• ืžื•ื“ื“ ื”ื™ื ื”ื•ื“ืขื” ืฉืชืฉืงืจ ื‘ืฉืงื˜.

`check:dialog-drag` ื ื“ืจืฉ ืœืชื™ืงื•ืŸ: ื”ื•ื ืคืชื— ืืช ื”ื“ื™ืืœื•ื’ ืขืœ ืกืžืŸ ืžื›ื•ื•ืฅ, ื•ืœื›ืŸ
โ€žEnter ืžืืฉืจโ€ ื ืžื“ื“ ืขืœ ื›ืคืชื•ืจ ืฉื ืขื•ืœ ืžืกื™ื‘ื” ืื—ืจืช. ื”ื•ื ืžืกืžืŸ ื˜ืงืกื˜ ืœืคื ื™, ื•ืžืคืจื™ื“
ืขื›ืฉื™ื• ื‘ื™ืŸ โ€žื”-Enter ืœื ื”ื’ื™ืขโ€ ืœื‘ื™ืŸ โ€žื”ื”ื—ืœื” ื ื›ืฉืœื”โ€ ืœืคื™ ืฉื•ืจืช ื”ืžืฆื‘.

ืžื•ื˜ืฆื™ื”, ืœืคื ื™ ืฉื ืกืžื›ื™ื ืขืœ ืื—ืช ืžื”ืŸ: ื›ืœ ืฉื•ืžืจ ื”ื•ืกืจ ื‘ืชื•ืจื• ื•ื ืžื“ื“ ืฉื”ื‘ื“ื™ืงื” ื ืฆื‘ืขืช
ืื“ื•ื โ€” ื”ืจืฉื™ืžื”, ื”ืฉืขืจ, ื”ื”ื•ื“ืขื”, ื”ืžืื–ื™ืŸ, ืžืขืงื‘ ื”ืžืกืžืš, ื•ืžื™ืคื•ื™ โ€žืื™ืŸ ืžืกืžืšโ€.
ืฉืœื•ืฉื™ื ื•ืื—ืช ื‘ื“ื™ืงื•ืช ื—ื“ืฉื•ืช (ืจื›ื™ื‘ + ื™ื—ื™ื“ื”). `npm test` ืžืœื: 3992 ืขื•ื‘ืจื•ืช;
typecheck ื ืงื™; ืฉื ื™ ื”ืฉืขืจื™ื ื™ืจื•ืงื™ื.

ืฉืžื•ื ื” ื‘ื“ื™ืงื•ืช ืงื™ื™ืžื•ืช ืฉื”ืจื›ื™ื‘ื• ืืช ื”ื“ื™ืืœื•ื’ ื‘ืœื™ ื‘ื—ื™ืจื” ืžืฆื”ื™ืจื•ืช ืขืœื™ื” ืขื›ืฉื™ื•
ื‘ืžืคื•ืจืฉ โ€” ื”ืชื ืื™ ื”ืžื•ืงื“ื ืฉืœื• ื”ื•ื ื˜ืงืกื˜ ืžืกื•ืžืŸ. `createSuperdocDouble` ืงื™ื‘ืœ
`setSelection`: ื”ื‘ื—ื™ืจื” ืื™ื ื” ืงื‘ื•ืขื” ื‘ื–ืžืŸ ื”ื”ืจื›ื‘ื”, ื•ืžืžืฉืง ืฉืงื•ืจื ืื•ืชื” ื™ื•ืชืจ
ืžืคืขื ืื—ืช ืื™ื ื• ื ื™ืชืŸ ืœื‘ื“ื™ืงื” ื‘ืœื™ ื–ื”.
`w:outline`, `w:shadow`, `w:emboss` and `w:imprint` are what the Font
dialog's "Effects" group writes, and Word still honours them โ€” a document
can carry them with no `w14:` counterpart anywhere. They round-tripped
correctly through import and export and were then dropped: the normalizer
never read them, so nothing downstream could paint them. Applying one to a
selection changed the file and left the page identical.

`w:dstrike` had the other half of the same problem. `normalizeRunAttrsFromOoxml`
already emitted `doubleStrike`, and it reached the painter โ€” the painter simply
never read it, so a double strikethrough was drawn as a single line.

- style-engine passes the four flags through as authored, including an
  explicit `false` (`<w:shadow w:val="0"/>` is how Word clears an inherited
  effect, and dropping the key would let a lower cascade layer switch it on).
- `RunMarks` carries them, next to `strike`, for every run kind.
- The DOM painter draws them, after `applyTextEffects` so it can see what an
  authored `w14:` effect already wrote. It stands aside for a `w14:textOutline`
  that actually painted a stroke, for a `w14:textFill`, and for a `w14:shadow`;
  it composes with a `w14:glow`, which is a different effect. The outline test
  reads what was written rather than whether the object exists, because
  `<w14:textOutline w14:w="0"><w14:noFill/></w14:textOutline>` is a present
  object that paints nothing.
- The outline empties the glyph through `-webkit-text-fill-color`, not
  `color`. The stroke is `currentColor`, and clearing `color` would take the
  stroke with it โ€” a run carrying `<w:outline/>` with Automatic color, which
  is exactly what the Font dialog writes, would have painted nothing at all.
  Formatting marks opt back out of both inherited properties, so the dot
  inside an outlined run is not emptied with the text around it.
- `doubleStrike` draws its own line: it is a separate mark and a separately
  settable run attribute, so a run carrying only `w:dstrike` still has a
  strikethrough. `render-line.ts` re-derives the decoration on the paths that
  lift an underline onto an overlay, so that rule is now shared rather than
  written twice.
- The five paint-invalidation signatures count them โ€”
  `textRunMergeSignature`, `hashRunVisualMarks`, `deriveBlockVersion`,
  `deriveParagraphBlockVersion` and `hashParagraphBlockForTableVersion`.
  Without those, an edit that flips only `<w:emboss/>` leaves the
  already-painted span on screen, and two adjacent runs differing only in one
  flag merge into a single span.

Outline is faithful โ€” a hairline stroke with the fill removed is what Word
draws. Shadow is a hard-edged offset copy. Emboss and imprint keep the
authored glyph color and put a light edge on one side and a dark edge on the
other; Word instead paints the glyph near the page color, which needs a
resolved page background that paint does not have, and guessing it wrong
turns the text invisible. The reasoning sits in the source.

CSS carries one `text-decoration-style` for the whole decoration set, so a
run that is both underlined and double-struck keeps the underline's authored
style and the strike stays single โ€” except on the overlay paths, where the
underline has already been lifted off the run and the double can be drawn.
Y-PLONI pushed a commit to Nathaniel-260/otzaria-word-editor that referenced this pull request Sep 7, 2026
ื“ื•ื•ื—: โ€žื›ืคืชื•ืจ ืžืชืงื“ื ืคื•ืชื— ืคืืค, ืื ื™ ื‘ื•ื—ืจ ืขื™ืฆื•ื‘, ืœื•ื—ืฅ, ื•ื–ื” ืœื ืขื•ืฉื” ื›ืœื•ืโ€.
ื ืžื“ื“ ื‘-Chrome ืืžื™ืชื™ ืขืœ ื”-dist ื”ืืจื•ื– โ€” ืคื™ืจื•ืง ื”-zip ืฉืœ `export.toDocx` ืžื•ืœ
`getComputedStyle` ืขืœ `.superdoc-text-run`, ื•ื‘ื”ืฉื•ื•ืืช ืฆื™ืœื•ื ืžืกืš ื‘ื™ื™ื˜-ื‘ื‘ื™ื™ื˜
ืฉืœ ื”ืฉื•ืจื” ืœืคื ื™ ื•ืื—ืจื™ โ€” ื•ื ืžืฆืื• ืฉื ื™ ื“ื‘ืจื™ื ืฉื•ื ื™ื ืœื’ืžืจื™ ืฉื ืจืื™ื ืœืžืฉืชืžืฉ ืื•ืชื• ื“ื‘ืจ.

**ื”ืื—ื“: ืื™ืŸ ื˜ืงืกื˜ ืžืกื•ืžืŸ.** `format.apply` ืžืงื‘ืœ SelectionTarget ื‘ืœื‘ื“, ื•ืขื“
ืขื›ืฉื™ื• ื”ืชืฉื•ื‘ื” ื”ื’ื™ืขื” ืจืง ื‘ืจื’ืข ื”โ€žืื™ืฉื•ืจโ€: ื”ื“ื™ืืœื•ื’ ื ืกื’ืจ, ืœืงื— ืื™ืชื• ืืช ืฉื‘ืขื”-ืขืฉืจ
ื”ืฉื“ื•ืช ืฉืžื•ืœืื•, ื•ื”ืฉืื™ืจ โ€žื™ืฉ ืœืกืžืŸ ื˜ืงืกื˜ ืชื—ื™ืœื”โ€ ื‘ืฉื•ืจืช ื”ืžืฆื‘. ื–ื• ืื™ื ื” ืžื’ื‘ืœืช ืžื ื•ืข
ืืœื ืกื“ืจ ืฉืœ ืžืžืฉืง, ื•ื”ื™ื ืชื•ืงื ื” ื‘ืฉืœื•ืฉื” ื—ืœืงื™ื:

1. `hasRangeSelection` (engine/font-advanced.ts) ื”ื™ื ืื•ืชื” ืงืจื™ืื” ื‘ื“ื™ื•ืง ืฉืœ
   `applyFontAdvanced` โ€” ืœื ืฉืืœื” ืฉื ื™ื™ื” ืฉืขืœื•ืœื” ืœืขื ื•ืช ืื—ืจืช โ€” ื•ื”ื™ื ื ืฉืืœืช
   ื‘ืคืชื™ื—ื”. ืืจื‘ืข ืชืฉื•ื‘ื•ืช ื•ืœื ืฉืชื™ื™ื: โ€žื™ืฉ ื˜ื•ื•ื—โ€, โ€žืื™ืŸ ื‘ื—ื™ืจื”โ€, โ€žืื™ืŸ
   `format.apply` ื‘ื’ืจืกื” ื”ื–ืืชโ€, ื•โ€žืื™ืŸ ืœื“ืขืชโ€ ืฉืื™ื ื• ื ื•ืขืœ ื“ื‘ืจ. โ€žืื™ืŸ ืžืกืžืšโ€
   ื ื›ืœืœ ื‘ืื—ืจื•ืŸ: ื‘ื™ืŸ ืžืกืžื›ื™ื ื•ื‘ื–ืžืŸ ื˜ืขื™ื ื” ื”ืžืกืžืš ื”ื•ื `null`, ื•ื”ื•ื“ืขื” ืฉืื•ืžืจืช ืฉื
   โ€žืื™ื ื• ื–ืžื™ืŸ ื‘ื’ืจืกื” ื–ื• ืฉืœ ื”ืžื ื•ืขโ€ ื”ื™ื ื˜ืขื ื” ืฉืงืจื™ืช ืขืœ ื”ื‘ื ื™ื™ืŸ.
2. **ื•ื”ื™ื ื ืฉืืœืช ืฉื•ื‘.** ื”ื“ื™ืืœื•ื’ ืื™ื ื• ื—ื•ืกื ืืช ื”ืžืกืžืš โ€” ืื™ืŸ ืžืื—ื•ืจื™ื• ืจืงืข, ื”ื•ื
   ื ื’ืจืจ ื‘ื›ื•ื•ื ื”, ื•ืืคืฉืจ ืœืœื—ื•ืฅ ื‘ื˜ืงืกื˜ ื‘ื–ืžืŸ ืฉื”ื•ื ืคืชื•ื—. ืชืฉื•ื‘ื” ืฉื ืงืจืื” ืคืขื ืื—ืช
   ืžืชื™ื™ืฉื ืช ื‘ืฉื ื™ ื”ื›ื™ื•ื•ื ื™ื, ื•ืฉื ื™ื”ื ืžื—ื–ื™ืจื™ื ืืช ื”ื‘ืื’: ื‘ื—ื™ืจื” ืฉื”ืชื›ื•ื•ืฆื” ืžืชื—ืช
   ืœื›ืคืชื•ืจ ืฉื ืฉืืจ ืคืชื•ื—, ืื• ื›ืคืชื•ืจ ืฉื ืขื•ืœ ืœื ืฆื— ืื—ืจื™ ืฉื”ืžืฉืชืžืฉ ืกื™ืžืŸ ื˜ืงืกื˜ ื›ืžื•
   ืฉื”ื”ื•ื“ืขื” ื‘ื™ืงืฉื”. ืžืื–ื™ืŸ `selectionchange` ืžื›ืกื” ืืช ืฉื™ื ื•ื™ื™ ื”ื‘ื—ื™ืจื”, ื•-`watch`
   ืขืœ ื”ืžืกืžืš ื”ืคืขื™ืœ ืžื›ืกื” ืžืขื‘ืจ ื‘ื™ืŸ ืœืฉื•ื ื™ื•ืช โ€” ืฉืื™ื ื• ืžื™ื™ืฆืจ `selectionchange`.
   ืคืก ื”ืชืฆื•ื’ื” ื”ืžืงื“ื™ืžื” ื ืงืจื ืžื—ื“ืฉ ื™ื—ื“ ืขื ื”ืชืฉื•ื‘ื”.
3. **ื•ื”ื“ื™ืืœื•ื’ ื ืกื’ืจ ืขืœ ื”ืฆืœื—ื” ื‘ืœื‘ื“.** ืงื•ื“ื ื”ื•ื ื ืกื’ืจ ืœืคื ื™ ืฉื”ื”ื—ืœื” ื™ืฆืื” ืœื“ืจืš.

**ื•ื”ืฉื ื™: ืคืงื“ื™ื ืฉื ื›ืชื‘ื™ื ื•ืื™ื ื ืžืฆื•ื™ืจื™ื.** ืžืกื’ืจืช ืœืชื•, ืฆืœ, ื—ืจื•ื˜ ื•ืฉืงื•ืข ื™ื•ืฆืื™ื
ืœ-docx ืงื ื•ื ื™ืช ื•ืžื•ืฆื’ื™ื ื‘-Word, ื•ื”ืขื•ืจืš ืื™ื ื• ืžืฆื™ื™ืจ ืืฃ ืื—ื“ ืžื”ื โ€” ืฆื™ืœื•ื ื”ืžืกืš
ื–ื”ื” ื‘ื™ื™ื˜-ื‘ื‘ื™ื™ื˜. โ€žืงื• ื—ื•ืฆื” ื›ืคื•ืœโ€ ืžืฆื•ื™ืจ, ืื‘ืœ ื›ืงื• ื‘ื•ื“ื“. ื•ื‘ืื•ืชื” ืจืฉืช ืคืงื“ื™ื ื’ื
โ€žืงืจื ื™ื ื’โ€ ื•โ€žื’ื•ื“ืœ ื”ื’ื•ืคืŸ ื”ืžื•ืจื›ื‘โ€ ืื™ื ื ืžืฆื•ื™ืจื™ื, ื•ื›ืš ื›ืœ ืžื—ืกื ื™ืช ื”ื›ืชื‘ ื”ืžื•ืจื›ื‘.
ืžื‘ื—ื™ื ืช ืžื™ ืฉืœื—ืฅ โ€žืื™ืฉื•ืจโ€ ื–ื” ื‘ื“ื™ื•ืง ื›ืžื• ืคืงื“ ืฉืื™ื ื• ืขื•ื‘ื“ โ€” ื•ืžื›ื™ื•ื•ืŸ ืฉื”ืคืขื•ืœื”
ืžื“ื•ื•ื—ืช ื”ืฆืœื—ื”, ื’ื ืฉื•ืจืช ื”ืžืฆื‘ ื ืฉืืจืช ืจื™ืงื”.

ืื™ืŸ ืžื” ืœืขืฉื•ืช ื‘ืฆื“ ืฉืœื ื•: `.superdoc-text-run` ืื™ื ื• ื ื•ืฉื ืฉื•ื ืกื™ืžืŸ ืœืžืืคื™ื™ื ื™
ื”ืจื™ืฆื”, ื•ืœื›ืŸ ืื™ืŸ ื•ื• ืœ-CSS ืžืงื•ืžื™, ื•ื”ืฉืœื‘ ื”ืžืงื“ื™ื (docx-preflight) ืจืฅ ื‘ืคืชื™ื—ื”
ื‘ืœื‘ื“ ื•ืœื ื”ื™ื” ื ื•ื’ืข ื‘ืขื™ืฆื•ื‘ ืฉืžื•ื—ืœ ืขื›ืฉื™ื• โ€” ืฉื–ื” ื‘ื“ื™ื•ืง ื”ืžืงืจื” ืฉื“ื•ื•ื—.

ืžื” ืฉื›ืŸ: ื”ื“ื™ืืœื•ื’ ืื•ืžืจ ืืช ื–ื”, ืžืชื—ืช ืœืคืก ื”ืชืฆื•ื’ื” ื”ืžืงื“ื™ืžื”, ื•ืจืง ืขืœ ืžื” ืฉื ื‘ื—ืจ.
ื”ื›ื™ืชื•ื‘ ืฉืœ ื”ืคืก ื”ืคืš ืœโ€žื›ืš ื™ื™ืจืื” ื‘-Word (ืงื™ืจื•ื‘)โ€ โ€” ื”ื•ื ืžืชื—ื™ื™ื‘ ืขืœ ื”ืงื•ื‘ืฅ,
ื•ื”ื”ื•ื“ืขื” ืฉืžืชื—ืชื™ื• ืื•ืžืจืช ืžื” ื”ืžืกืš ืœื ื™ืจืื”. ื”ื•ื“ืขื” ื•ืœื ืื–ื”ืจื”: ืฉื•ื ื“ื‘ืจ ืœื ื ื›ืฉืœ.

**ื”ืชื™ืงื•ืŸ ืขืฆืžื• ื ืฉืœื— ืœืžืขืœื”:** superdoc/docx-editor#3983. ืฉืชื™ ื”ืฉื›ื‘ื•ืช ืฉื‘ื•ืœืขื•ืช
ืื•ืชืจื• ื‘ืžื“ื™ื“ื” โ€” `dstrike` ื”ื•ื ืคืขืจ ืฉืœ ื”ืฆื™ื™ืจ ื‘ืœื‘ื“, ื•ืืจื‘ืขืช ื”ืื—ืจื™ื ื ื‘ืœืขื™ื
ื‘-`normalizeRunAttrsFromOoxml`. ื”ื’ืฉืจ ืืœ ื—ื•ื–ื” ื”ืคืจื™ืกื” ื’ื ืจื™, ื•ืœื›ืŸ ื›ืœ ื”ืชื™ืงื•ืŸ
ื™ื•ืฉื‘ ื‘ืงื•ื“ ื”-OSS. `w:kern` ื”ื•ืฉืืจ ื‘ื—ื•ืฅ ื‘ืžืคื•ืจืฉ: ืงืจื ื™ื ื’ ืžืฉื ื” ืืช ืจื•ื—ื‘ ื”ื–ื—ื™ืœื”
ืฉืœ ื”ื’ืœื™ืคื™ื, ื›ืœื•ืžืจ ืืช ื”ืžื“ื™ื“ื” ื•ืœื ืจืง ืืช ื”ืฆื™ื•ืจ. ื”ืจื™ืฉื•ื ื”ืžืœื ื‘-docs/engine-gaps.md.

**ืฉืขืจ ื—ื“ืฉ:** `npm run check:font-advanced` โ€” 5/5 ื‘-Chrome ืืžื™ืชื™. ืฉืœื•ืฉ
ืžืฉื•ืจื•ืชื™ื• ืžื•ื“ื“ื•ืช ืืช ื”ื”ื•ื“ืขื” ืขืฆืžื” ืžื•ืœ ื”ืคื™ืงืกืœื™ื, ืคืงื“ ืื—ื“ ืœื›ืœ ืจื™ืฆื” ื›ื“ื™ ืฉืืคืฉืจ
ื™ื”ื™ื” ืœื™ื™ื—ืก ืคื™ืงืกืœ ืœืคืงื“: ื‘ืจื’ืข ืฉื”ืžื ื•ืข ื™ืชื—ื™ืœ ืœืฆื™ื™ืจ ืฆืœ, ืื• ืงื• ื›ืคื•ืœ, ื”ืฉื•ืจื”
ืชื™ืคื•ืœ ื•ืชืืžืจ ืžื” ืœื”ื•ืจื™ื“ ืžื”ื”ื•ื“ืขื”. ื”ื•ื“ืขื” ืฉืื™ืฉ ืื™ื ื• ืžื•ื“ื“ ื”ื™ื ื”ื•ื“ืขื” ืฉืชืฉืงืจ ื‘ืฉืงื˜.

`check:dialog-drag` ื ื“ืจืฉ ืœืชื™ืงื•ืŸ: ื”ื•ื ืคืชื— ืืช ื”ื“ื™ืืœื•ื’ ืขืœ ืกืžืŸ ืžื›ื•ื•ืฅ, ื•ืœื›ืŸ
โ€žEnter ืžืืฉืจโ€ ื ืžื“ื“ ืขืœ ื›ืคืชื•ืจ ืฉื ืขื•ืœ ืžืกื™ื‘ื” ืื—ืจืช. ื”ื•ื ืžืกืžืŸ ื˜ืงืกื˜ ืœืคื ื™, ื•ืžืคืจื™ื“
ืขื›ืฉื™ื• ื‘ื™ืŸ โ€žื”-Enter ืœื ื”ื’ื™ืขโ€ ืœื‘ื™ืŸ โ€žื”ื”ื—ืœื” ื ื›ืฉืœื”โ€ ืœืคื™ ืฉื•ืจืช ื”ืžืฆื‘.

ืžื•ื˜ืฆื™ื”, ืœืคื ื™ ืฉื ืกืžื›ื™ื ืขืœ ืื—ืช ืžื”ืŸ: ื›ืœ ืฉื•ืžืจ ื”ื•ืกืจ ื‘ืชื•ืจื• ื•ื ืžื“ื“ ืฉื”ื‘ื“ื™ืงื” ื ืฆื‘ืขืช
ืื“ื•ื โ€” ื”ืจืฉื™ืžื”, ื”ืฉืขืจ, ื”ื”ื•ื“ืขื”, ื”ืžืื–ื™ืŸ, ืžืขืงื‘ ื”ืžืกืžืš, ื•ืžื™ืคื•ื™ โ€žืื™ืŸ ืžืกืžืšโ€.
ืฉืœื•ืฉื™ื ื•ืื—ืช ื‘ื“ื™ืงื•ืช ื—ื“ืฉื•ืช (ืจื›ื™ื‘ + ื™ื—ื™ื“ื”). `npm test` ืžืœื: 3992 ืขื•ื‘ืจื•ืช;
typecheck ื ืงื™; ืฉื ื™ ื”ืฉืขืจื™ื ื™ืจื•ืงื™ื.

ืฉืžื•ื ื” ื‘ื“ื™ืงื•ืช ืงื™ื™ืžื•ืช ืฉื”ืจื›ื™ื‘ื• ืืช ื”ื“ื™ืืœื•ื’ ื‘ืœื™ ื‘ื—ื™ืจื” ืžืฆื”ื™ืจื•ืช ืขืœื™ื” ืขื›ืฉื™ื•
ื‘ืžืคื•ืจืฉ โ€” ื”ืชื ืื™ ื”ืžื•ืงื“ื ืฉืœื• ื”ื•ื ื˜ืงืกื˜ ืžืกื•ืžืŸ. `createSuperdocDouble` ืงื™ื‘ืœ
`setSelection`: ื”ื‘ื—ื™ืจื” ืื™ื ื” ืงื‘ื•ืขื” ื‘ื–ืžืŸ ื”ื”ืจื›ื‘ื”, ื•ืžืžืฉืง ืฉืงื•ืจื ืื•ืชื” ื™ื•ืชืจ
ืžืคืขื ืื—ืช ืื™ื ื• ื ื™ืชืŸ ืœื‘ื“ื™ืงื” ื‘ืœื™ ื–ื”.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant