Skip to content

fix(style-engine): resolve bold and italic from the complex-script toggles - #3958

Open
Nathaniel-260 wants to merge 1 commit into
superdoc:mainfrom
Nathaniel-260:fix/complex-script-bold
Open

fix(style-engine): resolve bold and italic from the complex-script toggles#3958
Nathaniel-260 wants to merge 1 commit into
superdoc:mainfrom
Nathaniel-260:fix/complex-script-bold

Conversation

@Nathaniel-260

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

Copy link
Copy Markdown
Contributor

The bug

Hebrew and Arabic Word writes bold-for-complex-script as w:bCs alone whenever the
styled selection is single-script โ€” no w:b alongside it. Word renders such a run
bold. SuperDoc reads only w:b, so a document whose headings are bold in Word is
painted at normal weight.

This is the shape of a real reported document (a Hebrew Torah-study text). Its
heading 2โ€“heading 5 styles are:

<w:style w:type="paragraph" w:styleId="2">
  <w:name w:val="heading 2"/>
  <w:rPr>
    <w:rFonts w:ascii="FrankRuehl DP" w:hAnsi="FrankRuehl DP" w:cs="FrankRuehl DP"/>
    <w:bCs/>                      <!-- no <w:b/> -->
    <w:szCs w:val="28"/>
  </w:rPr>
</w:style>

and its runs carry <w:rtl/>.

Reproduce

Any Hebrew .docx whose headings were bolded in Word will do; the smallest recipe:

  1. In Word, type a Hebrew line, apply Heading 2, and press Ctrl+B while the
    selection is Hebrew only. Word writes <w:bCs/> into the style with no <w:b/>.
  2. Open the file in SuperDoc.

Word shows the heading bold; SuperDoc shows it at normal weight.

Measurement

Chrome, packaged dist, getComputedStyle on the run element inside .superdoc-line:

font-weight
the reported document, as-is 400 on all ten headings
the same document, <w:b/> completed next to each <w:bCs/> 700 on all ten
the same document, unchanged, with this patch applied to the compiled bundle and nothing else 700 on all ten

The change

normalizeRunAttrsFromOoxml is the seam. It already receives the full
RunProperties โ€” boldCs, iCs, cs, rtl โ€” and everything downstream carries a
single bold, so the formatting stack has to be selected here.

Per ECMA-376 Annex I, w:rPr/w:rtl (ยง17.3.2.30) and w:rPr/w:cs (ยง17.3.2.7) each
select the complex-script variants of the character-formatting toggles. That is the
stack selection contracts/src/direction-context.ts already describes:

This context is preservation-only in Wave 1a. Wave 1b implements the
stack-selection logic (resolveRunScriptContext returns whether to render with the
CS or Latin stack).

resolveRunScriptContext does not exist yet; this covers the two toggles.

Why the Latin toggle stays a fallback

Word decides the stack per character, not per run. A run carrying only w:b is bold
there for whatever Latin text it holds, and plenty of existing documents carry w:b
alone on right-to-left runs โ€” SuperDoc's own format.apply writes them that way.
Reading w:bCs strictly would turn those runs normal: a regression in the opposite
direction, and a wider one. Per-character parity needs the run text next to the
properties, which this layer does not receive.

Known limit

A run whose Hebrew is implied only by its text, with neither w:rtl nor w:cs set,
still resolves through the Latin toggles. Measured: such a run stays at 400 with this
patch. Word picks the stack from the Unicode script of the text in that case, which
needs information this layer does not have.

Out of scope, deliberately

w:szCs and w:rFonts/@cs are the other two members of the same stack. Both change
size and font resolution for every right-to-left run โ€” a much larger behavioural
change than the toggles, and one that deserves its own measurement.

Tests

Four cases added to normalize.test.ts: the CS toggles on a w:rtl run and on a
w:cs run, the Latin toggles kept when no complex-script signal is present, and the
fallback on a complex-script run with no CS variant.

What I ran locally (Windows, Node 22, Bun 1.3.13):

  • bun test in style-engine โ€” 182 pass
  • bun test in layout-engine (964), word-layout (172), common (121),
    geometry-utils (7) โ€” all pass
  • vp fmt --check on both changed files โ€” clean

Not run: the full pnpm test. pnpm will not start in my checkout, and
bun test in document-api panics with integer overflow โ€” it does the same on an
unmodified tree here, so it is environmental and unrelated to this change.


Fixes #3959.

โ€ฆggles

Hebrew and Arabic Word writes bold-for-complex-script as `w:bCs` alone
whenever the styled selection is single-script, without `w:b`. Word
renders such a run bold; SuperDoc read only `w:b`, so a document whose
headings are bold in Word painted them at normal weight.

Measured in Chrome on a real Hebrew document, against the packaged dist
with this change applied to the compiled bundle and nothing else: all ten
headings went from `font-weight: 400` to `700`. The document's heading
styles carry `<w:bCs/>` with no `<w:b/>`, and its runs carry `<w:rtl/>` โ€”
the shape Hebrew Word produces.

`normalizeRunAttrsFromOoxml` is the seam: it already receives the full
`RunProperties` โ€” `boldCs`, `iCs`, `cs`, `rtl` โ€” and the layout contract
downstream carries a single `bold`, so the stack has to be selected here.
Per ECMA-376 Annex I, `w:rPr/w:rtl` (ยง17.3.2.30) and `w:rPr/w:cs`
(ยง17.3.2.7) each select the complex-script variants of the toggles. This
is the `RunScriptContext` stack-selection that `direction-context.ts`
describes as Wave 1b.

The Latin toggle stays a fallback rather than being ignored. Word decides
the stack per character, not per run: a run carrying only `w:b` is bold
there for whatever Latin text it holds, and existing documents do carry
`w:b` alone on right-to-left runs. Reading `w:bCs` strictly would turn
those normal โ€” a regression in the opposite direction, and a wider one.
Per-character parity needs the run text next to the properties, which
this layer does not receive. That is also the measured limit of this
change: a run whose Hebrew is implied only by its text, with neither
`w:rtl` nor `w:cs` set, still resolves through the Latin toggles.

Out of scope, and deliberately: `w:szCs` and `w:rFonts/@cs` are the other
two members of the same stack. Both change size and font resolution for
every right-to-left run, which is a much larger behavioural change than
the toggles and deserves its own measurement.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more โ†’

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account โ†’

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us โ†’

@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.

No issues found across 2 files

Re-trigger cubic

palmoni5 pushed a commit to Y-PLONI/otzaria-word-editor that referenced this pull request Sep 3, 2026
ืกืงื™ืจื” ืื“ื•ื•ืจืกืจื™ืช ืžืฆืื” ืฉโ€žื“ืœื’ ืขืœ ืžื” ืฉื‘ืชื•ืš `w:rPrChange`โ€ ืœื ื”ืชืงื™ื™ื: ื”ืžื•ื ื”
ื ืฉื‘ืจ ืžืฉื ืžื—ื‘ืจ ืฉื™ืฉ ื‘ื• `>`, ืžืงื™ื“ื•ืžืช ืฉืื™ื ื” `w`, ื•ืžืกื’ื™ืจื” ืชืœื•ืฉื” ืฉื”ื•ืจื™ื“ื” ืื•ืชื•
ืžืชื—ืช ืœืืคืก ื•ืื– ื›ืชื‘ื” ืœืชื•ืš ื”ื”ื™ืกื˜ื•ืจื™ื” ื”ื‘ืื”. ื”ื’ืจื•ืขื” ืžื›ื•ืœืŸ โ€” ืžื•ื ื” ืฉื ืชืงืข ืขืœ 1
ืžื›ื‘ื” ืืช ื”ืชื™ืงื•ืŸ ืžืื•ืชื• ื‘ื™ื™ื˜ ื•ืขื“ ืกื•ืฃ ื”ื—ืœืง, ื‘ืฉืงื˜.

ื”ื›ืœืœ ื”ื™ื•ื ืื™ื ื• ืกื•ืคืจ ืชื’ื™ื: **`rPr` ื‘ืชื•ืš `rPr` ื”ื™ื ื”ื™ืกื˜ื•ืจื™ื”**. ื‘-`CT_RPr`
ื”ืื™ื‘ืจ ื”ื™ื—ื™ื“ ืฉืžื›ื™ืœ `rPr` ื”ื•ื `w:rPrChange`, ื•ืœื›ืŸ ื”ืงื™ื ื•ืŸ ื”ื•ื ื”ื”ื’ื“ืจื”. ืฉืœื•ืฉ
ื“ืจื›ื™ ื”ืฉื‘ื™ืจื” ื ืขืœืžื•ืช ืื™ืชื•.

ื‘ืื•ืชื” ืกืจื™ืงื” ืชื•ืงื ื• ืขื•ื“ ืฉืœื•ืฉื”: ื›ืชื™ื‘ื” ืœืชื•ืš ื”ืขืจืช XML ื•ืœืชื•ืš CDATA (ืขืจื™ื›ื” ืฉืœ
ื˜ืงืกื˜ ื”ืžืฉืชืžืฉ, ืœื ืฉืœ ืขื™ืฆื•ื‘), `w:val='0'` ื‘ืžืจื›ืื•ืช ื‘ื•ื“ื“ื•ืช ืฉื ืงืจื ื›โ€žื“ื•ืœืงโ€
ื•ื”ืคืš โ€žืœื ืžื•ื“ื’ืฉโ€ ืžืคื•ืจืฉ ืœืžื•ื“ื’ืฉ, ื•ืงื™ื“ื•ืžืช ืฉืื™ื ื” `w` โ€” ืฉื ื”ืชื™ืงื•ืŸ ื’ื ื”ื—ืžื™ืฅ
ืœื’ืžืจื™ ื•ื’ื ื”ื•ืกื™ืฃ `w:b` ืฉื ื™ื™ื” ืœืฆื“ `ns0:b` ืงื™ื™ืžืช. ื”ื”ื›ื ืกื” ื ื›ืชื‘ืช ืขื›ืฉื™ื•
ื‘ืงื™ื“ื•ืžืช ืฉืœ ื”-`bCs` ืฉื ืžืฆืื”.

ื•ื”ื”ืกืชื™ื™ื’ื•ื™ื•ืช ืžืกืงื™ืจืช ื”ืกื‘ื‘-ื”ืœื•ืš-ื—ื–ื•ืจ: `crc32` ืขื‘ืจ ืœ-slice-by-4 (384ms ืœ-
5.6MB ื”ื™ื• ื‘ืชื•ืš ื”ืฉืœื‘ ืฉืื™ ืืคืฉืจ ืœืงื˜ื•ืข), ื”ืคืœื˜ ืžื•ืจื›ื‘ ื‘-`join` ื•ืœื ื‘-`+=`,
ื•ื”ืžืฉืชืžืฉ ืจื•ืื” ืขื›ืฉื™ื• ื‘ืฉื•ืจืช ื”ืžืฆื‘ ืฉื”ืžืกืžืš ืฉืœื• ืฉื•ื ื” โ€” ืขื“ ื›ื” ื–ื” ื”ื™ื” `console`
ื‘ืœื‘ื“, ืขืœ ืฉื™ื ื•ื™ ืฉื ื›ืชื‘ ืœืงื•ื‘ืฅ ืฉืœื•.

53 ื‘ื“ื™ืงื•ืช ื™ื—ื™ื“ื” (ืž-33), ืฉืขืจ `bold-cs-qa` 7/7. ื‘ืฉืขืจ ื ืžืฆื ื•ืชื•ืงืŸ ื’ื ื‘ืื’
ืžื“ื™ื“ื” ืžืฉืœื•: ื›ืœ ืžืงืจื” ื ื’ื–ืจ ืžื”ืžืกืžืš ืฉืขืœ ื”ืžืกืš, ื›ืœื•ืžืจ ืžื”ืชื•ืฆืจ ืฉืœ ืงื•ื“ืžื•.

`w:rPrChange` ื ืžื“ื“ 2โ†’2: ื”ืžื ื•ืข ืฉื•ืžืจ ืืช ื”ื™ืกื˜ื•ืจื™ื™ืช ื”ืขื™ืฆื•ื‘, ื•ื”ืชื™ืงื•ืŸ ืื™ื ื•
ื ื•ื’ืข ื‘ื”. ื“ื™ื•ื•ื— ืงื•ื“ื ืขืœ โ€žื”ืžื ื•ืข ืžื•ื—ืง rPrChangeโ€ ื”ื™ื” ืจื’ืงืก ืฉื‘ื•ืจ ื‘ืฉืขืจ, ืœื
ืคืขืจ ื‘ืžื ื•ืข.

ื”ืชื™ืงื•ืŸ ื”ืžืงื‘ื™ืœ ื‘ืžื ื•ืข ืขืฆืžื•: superdoc/docx-editor#3958, ื•ืœืคื ื™ื•
superdoc/docx-editor#3959.
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.

Bold on Hebrew/Arabic runs is lost: w:bCs is parsed but never rendered

1 participant