You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
First, thank you — the engagement on #3959 and #3983 has been real, and the code around
this area is a pleasure to read: RunScriptContext in direction-context.ts states the
ECMA complex-script rule more clearly than the spec does. This issue is the half of that
rule that is still open.
Per ECMA-376 Annex I, a run resolving through the complex-script stack takes four
properties from the CS variants: bold from w:bCs, italic from w:iCs, size from w:szCs, and font from w:rFonts/@cs. #3959 (and PR #3958) covers the first two. The
other two are the ones that decide what the text looks like at all, and nothing reads
them — so a Hebrew or Arabic run is laid out and painted with the font and the size
belonging to the other script.
Size. Line 80 is const fontSize = resolveFontSize(props.fontSize); — the Latin w:sz, and only it. RunProperties.fontSizeCs is declared at style-engine/src/ooxml/types.ts:322 ("Complex script font size in half-points") and is
read nowhere in the package.
Font.resolveFontFamily (line 139) walks ascii → hAnsi → asciiTheme/hAnsiTheme → eastAsia → eastAsiaTheme → cs → cstheme → val,
so the cs slot at line 165 is reached only when the entire Latin track resolved to
nothing. The function's own comment says as much — the ascii/hAnsi track is "used by
Latin text — the only thing v2 paints generically right now".
And there is nowhere for a per-script value to travel.TextRunStyleAttrs
(style-engine/src/normalize/types.ts:24-25) carries one fontFamily and one fontSize, so the stack has to be chosen inside the normalizer — exactly where fix(style-engine): resolve bold and italic from the complex-script toggles #3958
now chooses it for bold and italic.
The contract already documents this as unfinished work, in RunScriptContext
(packages/layout-engine/contracts/src/direction-context.ts:150-160):
Per ECMA Annex I, when w:rtl is set or w:cs is set, the run's formatting comes from the
complex-script variants (bCs, iCs, szCs, rFonts/@cs). Otherwise it comes from the Latin
variants (b, i, sz, rFonts/@Ascii).
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 appears in that comment and nowhere else in the repository.
Why this bites harder than the bold leg
The two size slots usually hold different values in a real Hebrew document, because
Word keeps two independent size boxes in its font dialog and the Hebrew one is the one the
author sets. The heading style I quoted in #3959 has precisely that shape:
<w:rPr>
<w:rFontsw:ascii="FrankRuehl DP"w:hAnsi="FrankRuehl DP"w:cs="FrankRuehl DP"/>
<w:bCs/>
<w:szCsw:val="28"/> <!-- no w:sz at all -->
</w:rPr>
heading 2 there carries a CS size and no Latin size. With the Latin track as the only
source, the heading takes its size from whatever the cascade hands the Latin slot —
docDefaults — and comes out at body size. Bold is the second thing you notice about that
document; the size is the first.
The font slots diverge just as often, in the other direction: Word's Hebrew UI writes the
Hebrew face into w:cs and frequently leaves an unrelated Latin face in w:ascii/w:hAnsi. The Hebrew glyphs are then shaped in a font that was never chosen for
them — and where that face has no Hebrew coverage, in whatever the browser substitutes.
This is not paint-only. run-attrs feeds the size that layout measures, so line breaking
follows the wrong value too.
Steps to reproduce
cs-stack.docx from this gist — synthetic, generated from scratch, no dependencies,
Node 18+:
It puts 8pt Courier New in the Latin slots against 28pt Arial in the CS slots, so which
slot won is visible at a glance rather than something you have to measure. Six paragraphs,
each captioned in Latin with what Word does:
rFonts ascii="Courier New" cs="Arial", one size in both slots, w:rtl
Arial
Courier New
4
same as 1, selected by w:cs instead of w:rtl (§17.3.2.7)
Arial, 28pt
Courier New, 8pt
5
same as 1, carried by a paragraph style rather than direct formatting
Arial, 28pt
Courier New, 8pt
6
control — Latin run, same sz/szCs pair, neither w:rtl nor w:cs
Courier New, 8pt
Courier New, 8pt
Row 6 is the regression guard. Absence of both signals is not false: the stack then
follows the Unicode script of the run text, which this layer never sees, so a fix must
leave that line exactly as it is — the same reasoning usesComplexScriptToggles spells out
in #3958.
From Word instead of the gist: type a Hebrew line, set the complex-script size box in Font to something far from the Latin size box, save, open.
SuperDoc version
2.13.0 (@superdoc/docx-engine 0.12.0). Also main @ 9ae9f03 (2.14.0-next.2).
Browser
Chrome
Additional context
Where this sits against the open work: #3959 / PR #3958 fix the bold and italic legs of the
same stack, and the usesComplexScriptToggles(props) helper that PR adds is the natural
place to hang these two — props.fontSizeCs ?? props.fontSize for the size, and a CS-first
slot order for the font, each keeping the Latin value as fallback for the reason documented
there (Word decides the stack per character, and plenty of documents carry only the Latin
variant on an RTL run).
Happy to send that as a PR — on top of #3958, or standalone if you would rather land the
two changes separately. Just say which.
Apologies for the number of issues from me lately. I am building a Hebrew editor on
SuperDoc and this stack is the floor of it, so I keep landing on it. I am a student on an
old two-core laptop and each round of verification takes a while, so if you tell me which
of these is most useful to you I will do that one first.
What happened?
First, thank you — the engagement on #3959 and #3983 has been real, and the code around
this area is a pleasure to read:
RunScriptContextindirection-context.tsstates theECMA complex-script rule more clearly than the spec does. This issue is the half of that
rule that is still open.
Per ECMA-376 Annex I, a run resolving through the complex-script stack takes four
properties from the CS variants: bold from
w:bCs, italic fromw:iCs, size fromw:szCs, and font fromw:rFonts/@cs. #3959 (and PR #3958) covers the first two. Theother two are the ones that decide what the text looks like at all, and nothing reads
them — so a Hebrew or Arabic run is laid out and painted with the font and the size
belonging to the other script.
Where it stops, in the open tree (
main@9ae9f03)packages/layout-engine/style-engine/src/normalize/run-attrs.ts:const fontSize = resolveFontSize(props.fontSize);— the Latinw:sz, and only it.RunProperties.fontSizeCsis declared atstyle-engine/src/ooxml/types.ts:322("Complex script font size in half-points") and isread nowhere in the package.
resolveFontFamily(line 139) walksascii → hAnsi → asciiTheme/hAnsiTheme → eastAsia → eastAsiaTheme → cs → cstheme → val,so the
csslot at line 165 is reached only when the entire Latin track resolved tonothing. The function's own comment says as much — the ascii/hAnsi track is "used by
Latin text — the only thing v2 paints generically right now".
TextRunStyleAttrs(
style-engine/src/normalize/types.ts:24-25) carries onefontFamilyand onefontSize, so the stack has to be chosen inside the normalizer — exactly where fix(style-engine): resolve bold and italic from the complex-script toggles #3958now chooses it for bold and italic.
The contract already documents this as unfinished work, in
RunScriptContext(
packages/layout-engine/contracts/src/direction-context.ts:150-160):resolveRunScriptContextappears in that comment and nowhere else in the repository.Why this bites harder than the bold leg
The two size slots usually hold different values in a real Hebrew document, because
Word keeps two independent size boxes in its font dialog and the Hebrew one is the one the
author sets. The heading style I quoted in #3959 has precisely that shape:
heading 2there carries a CS size and no Latin size. With the Latin track as the onlysource, the heading takes its size from whatever the cascade hands the Latin slot —
docDefaults — and comes out at body size. Bold is the second thing you notice about that
document; the size is the first.
The font slots diverge just as often, in the other direction: Word's Hebrew UI writes the
Hebrew face into
w:csand frequently leaves an unrelated Latin face inw:ascii/w:hAnsi. The Hebrew glyphs are then shaped in a font that was never chosen forthem — and where that face has no Hebrew coverage, in whatever the browser substitutes.
This is not paint-only.
run-attrsfeeds the size that layout measures, so line breakingfollows the wrong value too.
Steps to reproduce
cs-stack.docxfrom this gist — synthetic, generated from scratch, no dependencies,Node 18+:
https://gist.github.com/Nathaniel-260/a95fb34889cf3077726f22c3d1109952
It puts 8pt Courier New in the Latin slots against 28pt Arial in the CS slots, so which
slot won is visible at a glance rather than something you have to measure. Six paragraphs,
each captioned in Latin with what Word does:
sz=16 szCs=56,rFonts ascii="Courier New" cs="Arial",w:rtlsz=16 szCs=56,w:rtl, norFontsrFonts ascii="Courier New" cs="Arial", one size in both slots,w:rtlw:csinstead ofw:rtl(§17.3.2.7)sz/szCspair, neitherw:rtlnorw:csRow 6 is the regression guard. Absence of both signals is not
false: the stack thenfollows the Unicode script of the run text, which this layer never sees, so a fix must
leave that line exactly as it is — the same reasoning
usesComplexScriptTogglesspells outin #3958.
From Word instead of the gist: type a Hebrew line, set the complex-script size box in
Font to something far from the Latin size box, save, open.
SuperDoc version
2.13.0 (
@superdoc/docx-engine0.12.0). Alsomain@9ae9f03(2.14.0-next.2).Browser
Chrome
Additional context
Where this sits against the open work: #3959 / PR #3958 fix the bold and italic legs of the
same stack, and the
usesComplexScriptToggles(props)helper that PR adds is the naturalplace to hang these two —
props.fontSizeCs ?? props.fontSizefor the size, and a CS-firstslot order for the font, each keeping the Latin value as fallback for the reason documented
there (Word decides the stack per character, and plenty of documents carry only the Latin
variant on an RTL run).
Happy to send that as a PR — on top of #3958, or standalone if you would rather land the
two changes separately. Just say which.
Apologies for the number of issues from me lately. I am building a Hebrew editor on
SuperDoc and this stack is the floor of it, so I keep landing on it. I am a student on an
old two-core laptop and each round of verification takes a while, so if you tell me which
of these is most useful to you I will do that one first.