fix(list-numbering): render hebrew1 and hebrew2 list markers - #3934
Conversation
listIndexMap had no handler for the OOXML hebrew1 and hebrew2 numbering
formats, so generateOrderedListIndex returned null for them and
formatNumberingTemplate fell back to ''. A Hebrew-numbered list rendered with
no marker at all through the plain branch, and with only the lvlText
punctuation through the levelNumberingFormats branch the v2 adapter takes.
Nothing surfaced the unsupported format: review-aware-numbering still reported
the paragraph as resolved, carrying the empty marker.
Add both formatters, pinned to Word 16 (ListFormat.ListString) rather than to
the spec, the way the Chinese formats were:
hebrew1 gematria numerals, with 15 and 16 rendered טו and טז at every
hundreds level (115, 215, 315) to avoid the divine-name spellings
יה and יו. Those eight values are the only places Word departs
from naive gematria.
hebrew2 22-letter alphabet counting, prefixing one ת per completed pass.
Word represents only 1-392 in either format and then restarts from א.
Measured across three full periods of a 1200-item list and at the w:start
values 0, 1, 380, 390, 391, 392, 393, 700, 780, 783, 784, 785 and 32760, so
the wrap keys off the counter value rather than the item position. The
PAGE-field path shares the 392 bound but emits a localized error string
instead of wrapping, which is why page numbering does not reuse these
formatters.
Every hebrew2 marker carries a leading U+200F and no hebrew1 marker does, on
both ListFormat.ListString and Field.Result.Text. Word emits it once per
formatted number rather than once per marker, so a three-level hebrew2 marker
carries three of them.
Values Word cannot number return '' instead of throwing: w:start="0" is
schema-valid (ST_DecimalNumber) and Word draws no marker for that item, while
'ת'.repeat(-1) would otherwise raise a RangeError inside the render path.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
caio-pizzol
left a comment
There was a problem hiding this comment.
Thanks for the thorough work here — this looks good.
We tested the exact PR locally and loaded DOCX fixtures through the current source runtime in Chromium. hebrew1 and hebrew2 render correctly, including RTL paragraphs and the leading U+200F for hebrew2. The decimal control still works too.
We’d keep U+200F for Word parity. We’ll handle the matching internal format support and release work on our side.
Approved from us. Thanks for the contribution!
Relates to #3933.
What changed
listIndexMapinshared/common/list-numbering/index.tshad no handler for theOOXML
hebrew1andhebrew2numbering formats, sogenerateOrderedListIndexreturned
nullandformatNumberingTemplatefell back to''.A Hebrew-numbered list therefore rendered with no number at all, in one of
two shapes depending on which branch of
formatNumberingTemplateran:lvlTextlevelNumberingFormats%1.א.א.levelNumberingFormats(the v2 adapter path)%1.א..א.The failure was silent —
review-aware-numberingstill reported the paragraphas
resolved, carrying the empty marker — which is likely why it wentunreported.
This adds both formatters and registers them. Nothing else changes; the diff is
purely additive (219 insertions, 0 deletions).
Reproduction
Measured on the packed plugin build, real engine, headless Chrome, loading a
DOCX whose
numbering.xmlcarries<w:numFmt w:val="hebrew1"/>andhebrew2:Before — rendered text of the page:
After:
The reproduction uses a generated DOCX fixture.
How the expected output was established
Not from the spec — from Word.
ECMA-376 §17.18.59names the two formats butdoes not define their glyph rules, and Word departs from the obvious reading in
three places. Everything below was read out of Microsoft Word 16
(build 16.0.20326) via COM, from a DOCX carrying each format:
Range.ListFormat.ListStringfor list markers,Field.Result.Textfor fields.Over 2,500 markers in total.
1. Word represents only 1-392, then restarts from א.
Verified across three full periods of a 1200-item list (resets at items 1, 393,
785, 1177) and at the
w:startvalues 0, 1, 380, 390, 391, 392, 393, 700, 780,783, 784, 785, 32760 and 32767-32770 — the last of those lands in the 83rd
period, so the wrap keys off the counter value, not the item position.
Separately, all 392 representable values were measured for both formats and
match byte-for-byte, so the mapping is closed rather than sampled. Ruled out: a 3-letter cap (392 =
שצבand 393 would beשצג, boththree letters), a 400 limit (
תnever appears inhebrew1at all), and anitem-count reset. 392 has no meaning in Hebrew numeration — the natural bounds
would be 399 or 400 — so it reads as an internal Word constant.
2. 15 and 16 are
טוandטז, at every hundreds level.יהandיוspell divine names and are avoided by convention. An exhaustivediff of Word against naive gematria over all 392 values gives exactly eight
differences: 15, 16, 115, 116, 215, 216, 315, 316. Word does not extend the
courtesy to other sensitive combinations — 270 renders
רע, 304שד, 344שמד— so neither do we.3. Every
hebrew2marker carries a leading U+200F; nohebrew1marker does.Confirmed on two independent Word APIs. Word emits it once per formatted
number, not once per marker: a three-level
hebrew2marker%1.%2.%3comesback as
U+200F א . U+200F ב . U+200F א. Placing the character inside theformatter reproduces that exactly. Flagging this explicitly for review — it is
an invisible control character and byte-parity with Word was the deciding
argument, so say the word if you would rather it were dropped.
Also measured: no final forms (
ךםןףץ) and no geresh/gershayim ever appear;Word draws no marker at all for value 0.
Out-of-range contract
Values Word cannot number return
''rather than throwing.w:start="0"isschema-valid and Word renders an empty marker for it, and
'ת'.repeat(-1)would otherwise raise a
RangeErrorinside the render path with nothing tocatch it. This matches the guards on
intToAlpha,ordinalTextFormatter,intToChineseCountingand the other neighbours in the file.Why page numbering is not in this PR
The same 392 bound applies to the
PAGEfield path, but its overflow behaviordiffers: Word substitutes a localized error string there instead of wrapping.
That is a separate decision and a separate public-API change, so it is a
separate PR.
Checks
pnpm -r --filter @superdoc/common --filter @superdoc/word-layout … test— passpnpm run check:types(tsc -b) — passvp lint,vp fmt --check— cleanformatNumberingTemplatebranches, the טו/טז rule at allfour hundreds levels, the wrap boundary, multilevel templates, the widest
marker (18 characters), and the non-positive/non-integer contract.
Two notes from setting up, unrelated to this change
pnpm testdoes not run on Windows:scripts/test.mjs:16callsspawnSync('pnpm.cmd', …)withoutshell: true, which returnsEINVALoncurrent Node (the CVE-2024-27980 fix). Same for
scripts/check-public-docapi.mjs. Happy to send a one-line PR.CONTRIBUTING.md:89-95scopes Bun topnpm ci:local, butshared/commonandpackages/word-layoutdeclare"test": "bun test", so plainpnpm testneeds Bun too.