@stll/folio-core 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/controller/fontReadiness.js +12 -7
  2. package/dist/controller/layoutPipeline.js +7 -3
  3. package/dist/docx/blockContentParser.js +2 -47
  4. package/dist/docx/fieldParser.d.ts +1 -1
  5. package/dist/docx/fieldParser.js +12 -4
  6. package/dist/docx/numberingParser.d.ts +3 -11
  7. package/dist/docx/numberingParser.js +26 -158
  8. package/dist/docx/ooxmlCounterFormatter.d.ts +8 -0
  9. package/dist/docx/ooxmlCounterFormatter.js +134 -0
  10. package/dist/docx/paragraphParser.js +3 -4
  11. package/dist/docx/serializer/tableSerializer.js +1 -1
  12. package/dist/docx/tableParser.js +2 -1
  13. package/dist/fields/bookmarkPages.js +1 -1
  14. package/dist/fields/evaluateField.js +1 -1
  15. package/dist/fields/fieldContext.d.ts +2 -0
  16. package/dist/fields/resolveFieldValues.js +5 -2
  17. package/dist/layout-bridge/convert/headerFooterLayout.js +7 -1
  18. package/dist/layout-bridge/convert/toFlowBlocks.js +27 -64
  19. package/dist/layout-engine/index.d.ts +3 -2
  20. package/dist/layout-engine/index.js +40 -16
  21. package/dist/layout-engine/measure/cache.js +5 -1
  22. package/dist/layout-engine/measure/complexScriptFormatting.d.ts +18 -0
  23. package/dist/layout-engine/measure/complexScriptFormatting.js +11 -0
  24. package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
  25. package/dist/layout-engine/measure/listMarkerWidth.js +25 -21
  26. package/dist/layout-engine/measure/measureContainer.js +31 -16
  27. package/dist/layout-engine/measure/measureHelpers.js +4 -0
  28. package/dist/layout-engine/measure/measureParagraph.js +13 -3
  29. package/dist/layout-engine/measure/measureTypes.d.ts +4 -0
  30. package/dist/layout-engine/paginator.d.ts +5 -3
  31. package/dist/layout-engine/paginator.js +34 -13
  32. package/dist/layout-engine/types.d.ts +34 -7
  33. package/dist/layout-painter/index.js +2 -1
  34. package/dist/layout-painter/renderPage.js +9 -3
  35. package/dist/layout-painter/renderParagraph.js +50 -42
  36. package/dist/layout-painter/renderUtils.d.ts +2 -0
  37. package/dist/paged-layout/sectionGeometry.d.ts +3 -2
  38. package/dist/paged-layout/sectionGeometry.js +17 -1
  39. package/dist/prosemirror/attrs/index.js +7 -6
  40. package/dist/prosemirror/conversion/fromProseDoc.js +5 -19
  41. package/dist/prosemirror/conversion/toProseDoc.js +5 -4
  42. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -3
  43. package/dist/prosemirror/extensions/features/ListExtension.js +1 -3
  44. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.d.ts +3 -2
  45. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +31 -17
  46. package/dist/prosemirror/extensions/nodes/TableExtension.js +1 -0
  47. package/dist/prosemirror/schema/marks.d.ts +8 -0
  48. package/dist/prosemirror/schema/nodes.d.ts +5 -7
  49. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -3
  50. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { expectFontFamilyMarkAttrs } from "../prosemirror/attrs/index.js";
1
+ import { expectFontFamilyMarkAttrs, expectParagraphAttrs } from "../prosemirror/attrs/index.js";
2
2
  import { parseFontFamilyList, resolveFontFamily } from "../utils/fontResolver.js";
3
3
  //#region src/controller/fontReadiness.ts
4
4
  function getDocumentFontSet() {
@@ -62,13 +62,15 @@ function collectInitialLayoutFontFaces(documentModel, pmDoc) {
62
62
  return Array.from(faces.values());
63
63
  }
64
64
  function addTextFormattingFontFaces(faces, formatting) {
65
- addLayoutFontFamilyFace(faces, formatting?.fontFamily, layoutDescriptorFromFormatting(formatting));
65
+ const standardDescriptor = layoutDescriptorFromFormatting(formatting);
66
+ const complexScriptDescriptor = layoutDescriptorFromEmphasis(formatting?.boldCs ?? formatting?.bold, formatting?.italicCs ?? formatting?.italic);
67
+ addLayoutFontFamilyFace(faces, formatting?.fontFamily, standardDescriptor, complexScriptDescriptor);
66
68
  }
67
69
  function collectProseMirrorFontFaces(faces, node, inheritedTextFormatting) {
68
70
  const paragraphDefaults = readParagraphDefaultTextFormatting(node);
69
71
  const textFormatting = paragraphDefaults ?? inheritedTextFormatting;
70
72
  if (paragraphDefaults) addTextFormattingFontFaces(faces, paragraphDefaults);
71
- if (node.attrs["listMarkerFontFamily"]) addLayoutFontFamilyFace(faces, node.attrs["listMarkerFontFamily"], REGULAR_LAYOUT_FONT_DESCRIPTOR);
73
+ if (node.type.name === "paragraph") addTextFormattingFontFaces(faces, expectParagraphAttrs(node).listMarkerFormatting);
72
74
  if (node.isText) {
73
75
  const descriptor = layoutDescriptorFromFormattingAndMarks(textFormatting, node.marks);
74
76
  addLayoutFontFamilyFace(faces, readFontFamilyMarkAttrs(node.marks) ?? textFormatting?.fontFamily ?? DEFAULT_LAYOUT_FONT_FAMILY, descriptor);
@@ -86,9 +88,12 @@ function readFontFamilyMarkAttrs(marks) {
86
88
  for (const mark of marks) if (mark.type.name === "fontFamily") return expectFontFamilyMarkAttrs(mark);
87
89
  }
88
90
  function layoutDescriptorFromFormatting(formatting) {
91
+ return layoutDescriptorFromEmphasis(formatting?.bold, formatting?.italic);
92
+ }
93
+ function layoutDescriptorFromEmphasis(bold, italic) {
89
94
  return {
90
- style: formatting?.italic ? "italic" : "normal",
91
- weight: formatting?.bold ? 700 : 400
95
+ style: italic ? "italic" : "normal",
96
+ weight: bold ? 700 : 400
92
97
  };
93
98
  }
94
99
  function layoutDescriptorFromFormattingAndMarks(formatting, marks) {
@@ -103,7 +108,7 @@ function layoutDescriptorFromFormattingAndMarks(formatting, marks) {
103
108
  weight: bold ? 700 : 400
104
109
  };
105
110
  }
106
- function addLayoutFontFamilyFace(faces, value, descriptor) {
111
+ function addLayoutFontFamilyFace(faces, value, descriptor, complexScriptDescriptor = descriptor) {
107
112
  if (typeof value === "string") {
108
113
  addLayoutFontFamilyNameFace(faces, value, descriptor);
109
114
  return;
@@ -112,7 +117,7 @@ function addLayoutFontFamilyFace(faces, value, descriptor) {
112
117
  const fontFamily = value;
113
118
  addLayoutFontFamilyFace(faces, fontFamily.ascii, descriptor);
114
119
  addLayoutFontFamilyFace(faces, fontFamily.hAnsi, descriptor);
115
- addLayoutFontFamilyFace(faces, fontFamily.cs, descriptor);
120
+ addLayoutFontFamilyFace(faces, fontFamily.cs, complexScriptDescriptor);
116
121
  addLayoutFontFamilyFace(faces, fontFamily.eastAsia, descriptor);
117
122
  }
118
123
  function addLayoutFontFamilyNameFace(faces, family, descriptor) {
@@ -18,7 +18,7 @@ import "../layout-engine/types.js";
18
18
  import { renderPages } from "../layout-painter/renderPage.js";
19
19
  import { tryBuildIncrementalMeasures } from "../paged-layout/incrementalMeasure.js";
20
20
  import { computePerBlockMeasureInputs } from "../paged-layout/sectionBlockWidths.js";
21
- import { getMargins, getPageSize, twipsToPixels } from "../paged-layout/sectionGeometry.js";
21
+ import { getMargins, getPageNumbering, getPageSize, twipsToPixels } from "../paged-layout/sectionGeometry.js";
22
22
  import { templatePreviewValuesKey } from "../prosemirror/plugins/templatePreviewValues.js";
23
23
  import { getDocumentWatermark } from "../watermark/index.js";
24
24
  //#region src/controller/layoutPipeline.ts
@@ -195,7 +195,8 @@ function runLayoutPipeline(deps, state, options = {}) {
195
195
  phaseStartedAt = performance.now();
196
196
  const bodyLayoutConfig = {
197
197
  pageSize,
198
- margins: initialBodyMargins
198
+ margins: initialBodyMargins,
199
+ pageNumbering: getPageNumbering(sectionProperties)
199
200
  };
200
201
  if (columns !== void 0) bodyLayoutConfig.columns = columns;
201
202
  const finalSectionProperties = document?.package.document.sections?.at(-1)?.properties;
@@ -213,7 +214,8 @@ function runLayoutPipeline(deps, state, options = {}) {
213
214
  authoredMargins: getMargins(finalSectionProperties),
214
215
  preparedHeader: finalHeaderForLayout,
215
216
  preparedFooter: finalFooterForLayout
216
- })
217
+ }),
218
+ pageNumbering: getPageNumbering(finalSectionProperties)
217
219
  } : bodyLayoutConfig;
218
220
  const finalColumns = getColumns(finalSectionProperties);
219
221
  if (finalColumns !== void 0) finalLayoutConfig.columns = finalColumns;
@@ -258,6 +260,7 @@ function runLayoutPipeline(deps, state, options = {}) {
258
260
  const nextLayoutOpts = {
259
261
  pageSize,
260
262
  margins: initialBodyMargins,
263
+ pageNumbering: bodyLayoutConfig.pageNumbering,
261
264
  pageGap,
262
265
  mirrorMargins
263
266
  };
@@ -273,6 +276,7 @@ function runLayoutPipeline(deps, state, options = {}) {
273
276
  count: 1,
274
277
  gap: 0
275
278
  };
279
+ nextLayoutOpts.finalPageNumbering = finalLayoutConfig.pageNumbering;
276
280
  }
277
281
  if (columns !== void 0) nextLayoutOpts.columns = columns;
278
282
  if (bodyBreakType !== void 0) nextLayoutOpts.bodyBreakType = bodyBreakType;
@@ -1,58 +1,13 @@
1
1
  import { parseBookmarkEnd, parseBookmarkStart } from "./bookmarkParser.js";
2
2
  import { appendBookmarkMarkerToLastParagraphInBlocks, prependBookmarkMarkersToFirstParagraphInBlocks } from "./bookmarkPlacement.js";
3
3
  import { convertBulletToUnicode } from "./bulletMarkers.js";
4
- import { padDecimal } from "./numberingParser.js";
4
+ import { formatOoxmlCounter } from "./ooxmlCounterFormatter.js";
5
5
  import { parseParagraph } from "./paragraphParser.js";
6
6
  import { enrichParagraphTextBoxes } from "./paragraphTextBoxEnrichment.js";
7
7
  import { parseSdtProperties } from "./sdtProperties.js";
8
8
  import { parseTable } from "./tableParser.js";
9
9
  import { elementToXml, findChild, getChildElements, getLocalName, mergeXmlnsDeclarations } from "./xmlParser.js";
10
10
  //#region src/docx/blockContentParser.ts
11
- const toRoman = (numParam) => {
12
- let num = numParam;
13
- const romanNumerals = [
14
- [1e3, "M"],
15
- [900, "CM"],
16
- [500, "D"],
17
- [400, "CD"],
18
- [100, "C"],
19
- [90, "XC"],
20
- [50, "L"],
21
- [40, "XL"],
22
- [10, "X"],
23
- [9, "IX"],
24
- [5, "V"],
25
- [4, "IV"],
26
- [1, "I"]
27
- ];
28
- let result = "";
29
- for (const [value, symbol] of romanNumerals) while (num >= value) {
30
- result += symbol;
31
- num -= value;
32
- }
33
- return result;
34
- };
35
- const toRepeatedLetter = (value, baseCodePoint) => {
36
- if (value <= 0) return "0";
37
- const zeroBased = value - 1;
38
- return String.fromCodePoint(baseCodePoint + zeroBased % 26).repeat(Math.floor(zeroBased / 26) + 1);
39
- };
40
- const formatNumber = (value, numFmt) => {
41
- switch (numFmt) {
42
- case "decimal": return String(value);
43
- case "decimalZero": return padDecimal(value, 2);
44
- case "decimalZero3": return padDecimal(value, 3);
45
- case "decimalZero4": return padDecimal(value, 4);
46
- case "decimalZero5": return padDecimal(value, 5);
47
- case "lowerLetter": return toRepeatedLetter(value, 97);
48
- case "upperLetter": return toRepeatedLetter(value, 65);
49
- case "lowerRoman": return toRoman(value).toLowerCase();
50
- case "upperRoman": return toRoman(value);
51
- case "bullet": return "•";
52
- case "none": return "";
53
- default: return String(value);
54
- }
55
- };
56
11
  const computeListMarker = (paragraph, { numbering, listCounters, abstractCounters, restartedNumIds, previousList, siblingNumIdsByAbstractNumId }) => {
57
12
  const listRendering = paragraph.listRendering;
58
13
  if (!listRendering || !numbering) {
@@ -137,7 +92,7 @@ const computeListMarker = (paragraph, { numbering, listCounters, abstractCounter
137
92
  if (computedMarker.includes(placeholder)) {
138
93
  const value = counters[lvl] ?? 0;
139
94
  const levelInfo = numbering.getLevel(numId, lvl);
140
- const formatted = formatNumber(value, useLegalNumbering ? "decimal" : levelInfo?.numFmt || "decimal");
95
+ const formatted = formatOoxmlCounter(value, useLegalNumbering ? "decimal" : levelInfo?.numFmt || "decimal");
141
96
  computedMarker = computedMarker.replaceAll(placeholder, formatted);
142
97
  }
143
98
  }
@@ -188,7 +188,7 @@ declare function isTocField(field: document_d_exports.Field): boolean;
188
188
  * @param instruction - Parsed instruction for format switches
189
189
  * @returns Formatted page number string
190
190
  */
191
- declare function computePageNumber(pageNumber: number, instruction?: ParsedFieldInstruction): string;
191
+ declare function computePageNumber(pageNumber: number, instruction?: ParsedFieldInstruction, sectionFormat?: string): string;
192
192
  /**
193
193
  * Format a date according to a format string
194
194
  *
@@ -345,16 +345,24 @@ function isTocField(field) {
345
345
  * @param instruction - Parsed instruction for format switches
346
346
  * @returns Formatted page number string
347
347
  */
348
- function computePageNumber(pageNumber, instruction) {
349
- if (!instruction) return String(pageNumber);
350
- const format = getFormatSwitch(instruction);
351
- if (!format) return String(pageNumber);
348
+ function computePageNumber(pageNumber, instruction, sectionFormat) {
349
+ const format = instruction ? getFormatSwitch(instruction) : void 0;
350
+ if (!format || format.toUpperCase() === "MERGEFORMAT") return formatSectionPageNumber(pageNumber, sectionFormat);
352
351
  switch (format.toUpperCase()) {
353
352
  case "ROMAN": return toRoman(pageNumber);
354
353
  case "ALPHABETIC": return toLetter(pageNumber);
355
354
  default: return String(pageNumber);
356
355
  }
357
356
  }
357
+ function formatSectionPageNumber(pageNumber, format) {
358
+ switch (format) {
359
+ case "upperRoman": return toRoman(pageNumber);
360
+ case "lowerRoman": return toRoman(pageNumber).toLowerCase();
361
+ case "upperLetter": return toLetter(pageNumber);
362
+ case "lowerLetter": return toLetter(pageNumber).toLowerCase();
363
+ default: return String(pageNumber);
364
+ }
365
+ }
358
366
  /**
359
367
  * Convert number to uppercase Roman numerals
360
368
  */
@@ -1,4 +1,5 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
+ import { formatOoxmlCounter, padDecimal } from "./ooxmlCounterFormatter.js";
2
3
  //#region src/docx/numberingParser.d.ts
3
4
  /**
4
5
  * Map of rId to numbering definitions
@@ -23,6 +24,7 @@ type NumberingMap = {
23
24
  * @returns NumberingMap with definitions and helper functions
24
25
  */
25
26
  declare function parseNumbering(numberingXml: string | null): NumberingMap;
27
+ declare const markerFormattingFromLevel: (formatting: document_d_exports.TextFormatting | undefined) => document_d_exports.ListMarkerFormatting | undefined;
26
28
  declare function getCachedNumberingMap(definitions: document_d_exports.NumberingDefinitions): NumberingMap;
27
29
  /**
28
30
  * Create a NumberingMap with helper functions
@@ -46,16 +48,6 @@ declare function computeListRendering(numPr: {
46
48
  }, numbering: NumberingMap): (document_d_exports.ListRendering & {
47
49
  levelStarts: number[];
48
50
  }) | null;
49
- /**
50
- * Format a number according to the specified format
51
- *
52
- * @param num - The number to format
53
- * @param format - The number format
54
- * @returns Formatted string
55
- */
56
- declare function formatNumber(num: number, format: document_d_exports.NumberFormat): string;
57
- /** Zero-pad a counter to `width` digits ("decimalZero" family, §17.18.59). */
58
- declare function padDecimal(num: number, width: number): string;
59
51
  /**
60
52
  * Compare two `numPr` references by value — `numId` plus `ilvl` (a missing
61
53
  * `ilvl` is level 0). Used for the style-sourced-numbering provenance check;
@@ -89,4 +81,4 @@ declare function getBulletCharacter(level: document_d_exports.ListLevel): string
89
81
  */
90
82
  declare function isBulletLevel(level: document_d_exports.ListLevel): boolean;
91
83
  //#endregion
92
- export { NumberingMap, computeListRendering, createNumberingMap, formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, numPrEqual, padDecimal, parseNumbering, renderListMarker };
84
+ export { NumberingMap, computeListRendering, createNumberingMap, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, markerFormattingFromLevel, numPrEqual, padDecimal, parseNumbering, renderListMarker };
@@ -1,4 +1,6 @@
1
- import { FontHintSchema, LevelSuffixSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
1
+ import { formatOoxmlCounter, padDecimal } from "./ooxmlCounterFormatter.js";
2
+ import { LevelSuffixSchema, narrowEnum } from "./parserEnums.js";
3
+ import { parseRunProperties } from "./runParser.js";
2
4
  import { findChild, findChildren, getAttribute, parseBooleanElement, parseNumericAttribute, parseXmlDocument } from "./xmlParser.js";
3
5
  //#region src/docx/numberingParser.ts
4
6
  const NUMBER_FORMAT_MAP = {
@@ -314,7 +316,10 @@ function parseListLevel(element) {
314
316
  };
315
317
  }
316
318
  if (pPrEl) level.pPr = parseLevelParagraphProps(pPrEl);
317
- if (rPrEl) level.rPr = parseLevelRunProps(rPrEl);
319
+ if (rPrEl) {
320
+ const runProperties = parseRunProperties(rPrEl, null);
321
+ if (runProperties) level.rPr = runProperties;
322
+ }
318
323
  return level;
319
324
  }
320
325
  /**
@@ -427,79 +432,21 @@ function parseTabLeader(val) {
427
432
  default: return;
428
433
  }
429
434
  }
430
- /**
431
- * Parse run properties for a list level (subset of full rPr)
432
- * Main concern: fonts for bullet characters
433
- */
434
- function parseLevelRunProps(rPr) {
435
- const formatting = {};
436
- let rFontsEl;
437
- let szEl;
438
- let colorEl;
439
- let bEl;
440
- let iEl;
441
- let vanishEl;
442
- for (const child of rPr.elements ?? []) {
443
- if (child.type !== "element") continue;
444
- switch (child.name) {
445
- case "w:rFonts":
446
- rFontsEl ??= child;
447
- break;
448
- case "w:sz":
449
- szEl ??= child;
450
- break;
451
- case "w:color":
452
- colorEl ??= child;
453
- break;
454
- case "w:b":
455
- bEl ??= child;
456
- break;
457
- case "w:i":
458
- iEl ??= child;
459
- break;
460
- case "w:vanish":
461
- vanishEl ??= child;
462
- break;
463
- default: break;
464
- }
465
- }
466
- if (rFontsEl) {
467
- const ascii = getAttribute(rFontsEl, "w", "ascii");
468
- const hAnsi = getAttribute(rFontsEl, "w", "hAnsi");
469
- const eastAsia = getAttribute(rFontsEl, "w", "eastAsia");
470
- const cs = getAttribute(rFontsEl, "w", "cs");
471
- const hint = narrowEnum(getAttribute(rFontsEl, "w", "hint"), FontHintSchema);
472
- formatting.fontFamily = {
473
- ...ascii != null ? { ascii } : {},
474
- ...hAnsi != null ? { hAnsi } : {},
475
- ...eastAsia != null ? { eastAsia } : {},
476
- ...cs != null ? { cs } : {},
477
- ...hint !== void 0 ? { hint } : {}
478
- };
479
- }
480
- if (szEl) {
481
- const size = parseNumericAttribute(szEl, "w", "val");
482
- if (size !== void 0) formatting.fontSize = size;
483
- }
484
- if (colorEl) {
485
- const val = getAttribute(colorEl, "w", "val");
486
- const validatedThemeColor = narrowEnum(getAttribute(colorEl, "w", "themeColor"), ThemeColorSlotSchema);
487
- if (val === "auto") formatting.color = { auto: true };
488
- else if (validatedThemeColor) {
489
- const themeTint = getAttribute(colorEl, "w", "themeTint");
490
- const themeShade = getAttribute(colorEl, "w", "themeShade");
491
- formatting.color = {
492
- themeColor: validatedThemeColor,
493
- ...themeTint != null ? { themeTint } : {},
494
- ...themeShade != null ? { themeShade } : {}
495
- };
496
- } else if (val) formatting.color = { rgb: val };
497
- }
498
- if (bEl) formatting.bold = parseBooleanElement(bEl);
499
- if (iEl) formatting.italic = parseBooleanElement(iEl);
500
- if (vanishEl) formatting.hidden = parseBooleanElement(vanishEl);
501
- return formatting;
502
- }
435
+ const markerFormattingFromLevel = (formatting) => {
436
+ if (!formatting) return;
437
+ const markerFormatting = {
438
+ ...formatting.fontFamily !== void 0 ? { fontFamily: formatting.fontFamily } : {},
439
+ ...formatting.fontSize !== void 0 ? { fontSize: formatting.fontSize } : {},
440
+ ...formatting.fontSizeCs !== void 0 ? { fontSizeCs: formatting.fontSizeCs } : {},
441
+ ...formatting.bold !== void 0 ? { bold: formatting.bold } : {},
442
+ ...formatting.boldCs !== void 0 ? { boldCs: formatting.boldCs } : {},
443
+ ...formatting.italic !== void 0 ? { italic: formatting.italic } : {},
444
+ ...formatting.italicCs !== void 0 ? { italicCs: formatting.italicCs } : {},
445
+ ...formatting.rtl !== void 0 ? { rtl: formatting.rtl } : {},
446
+ ...formatting.cs !== void 0 ? { cs: formatting.cs } : {}
447
+ };
448
+ return Object.keys(markerFormatting).length > 0 ? markerFormatting : void 0;
449
+ };
503
450
  /**
504
451
  * Per-definitions cache for `createNumberingMap`. Style application rebuilds
505
452
  * the lookup map on every picker click otherwise; the definitions object is
@@ -602,10 +549,8 @@ function computeListRendering(numPr, numbering) {
602
549
  };
603
550
  if (level.isLgl) rendering.isLegal = true;
604
551
  if (level.rPr?.hidden) rendering.markerHidden = true;
605
- const markerFont = level.rPr?.fontFamily?.ascii || level.rPr?.fontFamily?.hAnsi;
606
- if (markerFont) rendering.markerFontFamily = markerFont;
607
- if (level.rPr?.fontSize) rendering.markerFontSize = level.rPr.fontSize / 2;
608
- if (level.rPr?.bold !== void 0) rendering.markerBold = level.rPr.bold;
552
+ const markerFormatting = markerFormattingFromLevel(level.rPr);
553
+ if (markerFormatting) rendering.markerFormatting = markerFormatting;
609
554
  if (level.rPr?.allCaps) rendering.markerAllCaps = true;
610
555
  if (level.lvlJc) rendering.markerAlignment = level.lvlJc;
611
556
  if (level.suffix) rendering.markerSuffix = level.suffix;
@@ -614,37 +559,6 @@ function computeListRendering(numPr, numbering) {
614
559
  return rendering;
615
560
  }
616
561
  /**
617
- * Format a number according to the specified format
618
- *
619
- * @param num - The number to format
620
- * @param format - The number format
621
- * @returns Formatted string
622
- */
623
- function formatNumber(num, format) {
624
- switch (format) {
625
- case "decimal": return num.toString();
626
- case "decimalZero": return padDecimal(num, 2);
627
- case "decimalZero3": return padDecimal(num, 3);
628
- case "decimalZero4": return padDecimal(num, 4);
629
- case "decimalZero5": return padDecimal(num, 5);
630
- case "upperRoman": return toRoman(num).toUpperCase();
631
- case "lowerRoman": return toRoman(num).toLowerCase();
632
- case "upperLetter": return toLetter(num).toUpperCase();
633
- case "lowerLetter": return toLetter(num).toLowerCase();
634
- case "ordinal": return toOrdinal(num);
635
- case "bullet": return "•";
636
- case "none": return "";
637
- case "decimalEnclosedParen": return `(${num})`;
638
- case "numberInDash": return `-${num}-`;
639
- default: return num.toString();
640
- }
641
- }
642
- /** Zero-pad a counter to `width` digits ("decimalZero" family, §17.18.59). */
643
- function padDecimal(num, width) {
644
- if (num < 0) return num.toString();
645
- return num.toString().padStart(width, "0");
646
- }
647
- /**
648
562
  * Compare two `numPr` references by value — `numId` plus `ilvl` (a missing
649
563
  * `ilvl` is level 0). Used for the style-sourced-numbering provenance check;
650
564
  * avoids JSON.stringify equality, which is sensitive to key order.
@@ -654,52 +568,6 @@ function numPrEqual(a, b) {
654
568
  return a.numId === b.numId && (a.ilvl ?? 0) === (b.ilvl ?? 0);
655
569
  }
656
570
  /**
657
- * Convert number to Roman numerals
658
- */
659
- function toRoman(num) {
660
- if (num <= 0 || num > 3999) return num.toString();
661
- const romanNumerals = [
662
- [1e3, "m"],
663
- [900, "cm"],
664
- [500, "d"],
665
- [400, "cd"],
666
- [100, "c"],
667
- [90, "xc"],
668
- [50, "l"],
669
- [40, "xl"],
670
- [10, "x"],
671
- [9, "ix"],
672
- [5, "v"],
673
- [4, "iv"],
674
- [1, "i"]
675
- ];
676
- let result = "";
677
- let remaining = num;
678
- for (const [value, numeral] of romanNumerals) while (remaining >= value) {
679
- result += numeral;
680
- remaining -= value;
681
- }
682
- return result;
683
- }
684
- function toLetter(num) {
685
- if (num <= 0) return "";
686
- const zeroBased = num - 1;
687
- return String.fromCodePoint(97 + zeroBased % 26).repeat(Math.floor(zeroBased / 26) + 1);
688
- }
689
- /**
690
- * Convert number to ordinal (1st, 2nd, 3rd, ...)
691
- */
692
- function toOrdinal(num) {
693
- const suffix = [
694
- "th",
695
- "st",
696
- "nd",
697
- "rd"
698
- ];
699
- const v = num % 100;
700
- return num + (suffix[(v - 20) % 10] ?? suffix[v] ?? suffix[0] ?? "th");
701
- }
702
- /**
703
571
  * Render list marker text by replacing placeholders with formatted numbers
704
572
  *
705
573
  * @param lvlText - The level text pattern (e.g., "%1.", "%1.%2")
@@ -713,7 +581,7 @@ function renderListMarker(lvlText, counters, formats) {
713
581
  const placeholder = `%${i}`;
714
582
  if (result.includes(placeholder)) {
715
583
  const counterIndex = i - 1;
716
- const formatted = formatNumber(counters[counterIndex] ?? 1, formats[counterIndex] ?? "decimal");
584
+ const formatted = formatOoxmlCounter(counters[counterIndex] ?? 1, formats[counterIndex] ?? "decimal");
717
585
  result = result.replaceAll(placeholder, formatted);
718
586
  }
719
587
  }
@@ -742,4 +610,4 @@ function isBulletLevel(level) {
742
610
  return level.numFmt === "bullet" || level.numFmt === "none";
743
611
  }
744
612
  //#endregion
745
- export { computeListRendering, createNumberingMap, formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, numPrEqual, padDecimal, parseNumbering, renderListMarker };
613
+ export { computeListRendering, createNumberingMap, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, markerFormattingFromLevel, numPrEqual, padDecimal, parseNumbering, renderListMarker };
@@ -0,0 +1,8 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ //#region src/docx/ooxmlCounterFormatter.d.ts
3
+ /** Zero-pad a counter to `width` digits (the `decimalZero` family). */
4
+ declare const padDecimal: (value: number, width: number) => string;
5
+ /** Format one OOXML numbering counter using Microsoft Word display semantics. */
6
+ declare const formatOoxmlCounter: (value: number, format: document_d_exports.NumberFormat | undefined) => string;
7
+ //#endregion
8
+ export { formatOoxmlCounter, padDecimal };
@@ -0,0 +1,134 @@
1
+ //#region src/docx/ooxmlCounterFormatter.ts
2
+ const ROMAN_PAIRS = [
3
+ [1e3, "M"],
4
+ [900, "CM"],
5
+ [500, "D"],
6
+ [400, "CD"],
7
+ [100, "C"],
8
+ [90, "XC"],
9
+ [50, "L"],
10
+ [40, "XL"],
11
+ [10, "X"],
12
+ [9, "IX"],
13
+ [5, "V"],
14
+ [4, "IV"],
15
+ [1, "I"]
16
+ ];
17
+ const LATIN_LOWER_SEQUENCE = [..."abcdefghijklmnopqrstuvwxyz"];
18
+ const ARABIC_ALPHA_SEQUENCE = [..."أبتثجحخدذرزسشصضطظعغفقكلمنهوي"];
19
+ const ARABIC_ABJAD_SEQUENCE = [..."أبجدهوزحطيكلمنسعفصقرشتثخذضظغ"];
20
+ const ZERO_WIDTH_NON_JOINER = "‌";
21
+ const MAX_REPEATED_COUNTER_LENGTH = 1024;
22
+ const toRoman = (value) => {
23
+ if (value <= 0 || value > 3999) return String(value);
24
+ let remaining = value;
25
+ let result = "";
26
+ for (const [number, numeral] of ROMAN_PAIRS) while (remaining >= number) {
27
+ result += numeral;
28
+ remaining -= number;
29
+ }
30
+ return result;
31
+ };
32
+ const toRepeatedSequence = (value, sequence) => {
33
+ if (!Number.isSafeInteger(value) || value <= 0) return "";
34
+ const zeroBased = value - 1;
35
+ const character = sequence.at(zeroBased % sequence.length);
36
+ const repetitions = Math.floor(zeroBased / sequence.length) + 1;
37
+ if (repetitions > MAX_REPEATED_COUNTER_LENGTH) return;
38
+ return character?.repeat(repetitions) ?? "";
39
+ };
40
+ const toOrdinal = (value) => {
41
+ const suffixes = [
42
+ "th",
43
+ "st",
44
+ "nd",
45
+ "rd"
46
+ ];
47
+ const remainder = value % 100;
48
+ return `${value}${suffixes[(remainder - 20) % 10] ?? suffixes[remainder] ?? "th"}`;
49
+ };
50
+ /** Zero-pad a counter to `width` digits (the `decimalZero` family). */
51
+ const padDecimal = (value, width) => {
52
+ if (value < 0) return String(value);
53
+ return String(value).padStart(width, "0");
54
+ };
55
+ /** Format one OOXML numbering counter using Microsoft Word display semantics. */
56
+ const formatOoxmlCounter = (value, format) => {
57
+ if (!Number.isFinite(value)) return "";
58
+ switch (format ?? "decimal") {
59
+ case "decimal":
60
+ case "cardinalText":
61
+ case "ordinalText":
62
+ case "hex":
63
+ case "chicago":
64
+ case "ideographDigital":
65
+ case "japaneseCounting":
66
+ case "aiueo":
67
+ case "iroha":
68
+ case "decimalFullWidth":
69
+ case "decimalHalfWidth":
70
+ case "japaneseLegal":
71
+ case "japaneseDigitalTenThousand":
72
+ case "decimalEnclosedCircle":
73
+ case "decimalFullWidth2":
74
+ case "aiueoFullWidth":
75
+ case "irohaFullWidth":
76
+ case "ganada":
77
+ case "chosung":
78
+ case "decimalEnclosedFullstop":
79
+ case "decimalEnclosedCircleChinese":
80
+ case "ideographEnclosedCircle":
81
+ case "ideographTraditional":
82
+ case "ideographZodiac":
83
+ case "ideographZodiacTraditional":
84
+ case "taiwaneseCounting":
85
+ case "ideographLegalTraditional":
86
+ case "taiwaneseCountingThousand":
87
+ case "taiwaneseDigital":
88
+ case "chineseCounting":
89
+ case "chineseLegalSimplified":
90
+ case "chineseCountingThousand":
91
+ case "koreanDigital":
92
+ case "koreanCounting":
93
+ case "koreanLegal":
94
+ case "koreanDigital2":
95
+ case "vietnameseCounting":
96
+ case "russianLower":
97
+ case "russianUpper":
98
+ case "hebrew1":
99
+ case "hebrew2":
100
+ case "hindiVowels":
101
+ case "hindiConsonants":
102
+ case "hindiNumbers":
103
+ case "hindiCounting":
104
+ case "thaiLetters":
105
+ case "thaiNumbers":
106
+ case "thaiCounting": return String(value);
107
+ case "decimalZero": return padDecimal(value, 2);
108
+ case "decimalZero3": return padDecimal(value, 3);
109
+ case "decimalZero4": return padDecimal(value, 4);
110
+ case "decimalZero5": return padDecimal(value, 5);
111
+ case "upperRoman": return toRoman(value);
112
+ case "lowerRoman": return toRoman(value).toLowerCase();
113
+ case "upperLetter": return (toRepeatedSequence(value, LATIN_LOWER_SEQUENCE) ?? String(value)).toUpperCase();
114
+ case "lowerLetter": return toRepeatedSequence(value, LATIN_LOWER_SEQUENCE) ?? String(value);
115
+ case "ordinal": return toOrdinal(value);
116
+ case "bullet": return "•";
117
+ case "none": return "";
118
+ case "decimalEnclosedParen": return `(${value})`;
119
+ case "numberInDash": return `-${value}-`;
120
+ case "arabicAlpha": {
121
+ const counter = toRepeatedSequence(value, ARABIC_ALPHA_SEQUENCE);
122
+ if (counter === void 0) return String(value);
123
+ return counter ? `${counter}${ZERO_WIDTH_NON_JOINER}` : "";
124
+ }
125
+ case "arabicAbjad": {
126
+ const counter = toRepeatedSequence(value, ARABIC_ABJAD_SEQUENCE);
127
+ if (counter === void 0) return String(value);
128
+ return counter ? `${ZERO_WIDTH_NON_JOINER}${counter}` : "";
129
+ }
130
+ default: return String(value);
131
+ }
132
+ };
133
+ //#endregion
134
+ export { formatOoxmlCounter, padDecimal };
@@ -2,6 +2,7 @@ import { isValidHexId } from "../utils/hexId.js";
2
2
  import { parseBookmarkEnd as parseBookmarkEnd$1, parseBookmarkStart as parseBookmarkStart$1 } from "./bookmarkParser.js";
3
3
  import { parseFieldType } from "./fieldParser.js";
4
4
  import { parseHyperlink as parseHyperlink$1 } from "./hyperlinkParser.js";
5
+ import { markerFormattingFromLevel } from "./numberingParser.js";
5
6
  import { BorderStyleSchema, FrameWrapSchema, FrameXAlignSchema, FrameYAlignSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, TabLeaderSchema, TabStopAlignmentSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
6
7
  import { consolidateParagraphContent } from "./runConsolidator.js";
7
8
  import { parseRun, parseRunProperties } from "./runParser.js";
@@ -1016,10 +1017,8 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
1016
1017
  if (level.isLgl) listRendering.isLegal = true;
1017
1018
  listRendering.numFmt = level.isLgl ? "decimal" : level.numFmt;
1018
1019
  if (level.rPr?.hidden) listRendering.markerHidden = true;
1019
- const markerFont = level.rPr?.fontFamily?.ascii || level.rPr?.fontFamily?.hAnsi;
1020
- if (markerFont) listRendering.markerFontFamily = markerFont;
1021
- if (level.rPr?.fontSize) listRendering.markerFontSize = level.rPr.fontSize / 2;
1022
- if (level.rPr?.bold !== void 0) listRendering.markerBold = level.rPr.bold;
1020
+ const markerFormatting = markerFormattingFromLevel(level.rPr);
1021
+ if (markerFormatting) listRendering.markerFormatting = markerFormatting;
1023
1022
  if (level.rPr?.allCaps) listRendering.markerAllCaps = true;
1024
1023
  if (level.lvlJc) listRendering.markerAlignment = level.lvlJc;
1025
1024
  if (level.suffix) listRendering.markerSuffix = level.suffix;
@@ -134,7 +134,7 @@ function serializeTableFormatting(formatting, propertyChanges) {
134
134
  if (formatting.styleId) parts.push(`<w:tblStyle w:val="${escapeXml(formatting.styleId)}"/>`);
135
135
  const floatingXml = serializeFloatingTableProperties(formatting.floating);
136
136
  if (floatingXml) parts.push(floatingXml);
137
- if (formatting.bidi) parts.push("<w:bidiVisual/>");
137
+ if (formatting.bidi !== void 0) parts.push(formatting.bidi ? "<w:bidiVisual/>" : "<w:bidiVisual w:val=\"0\"/>");
138
138
  const widthXml = serializeMeasurement(formatting.width, "tblW");
139
139
  if (widthXml) parts.push(widthXml);
140
140
  if (formatting.justification) parts.push(`<w:jc w:val="${formatting.justification}"/>`);
@@ -284,7 +284,8 @@ function parseTableProperties(tblPrElement) {
284
284
  }
285
285
  const floating = parseFloatingTableProperties(findChild(tblPrElement, "w", "tblpPr"));
286
286
  if (floating) formatting.floating = floating;
287
- if (parseBooleanElement(findChild(tblPrElement, "w", "bidiVisual"))) formatting.bidi = true;
287
+ const bidiVisual = findChild(tblPrElement, "w", "bidiVisual");
288
+ if (bidiVisual) formatting.bidi = parseBooleanElement(bidiVisual);
288
289
  if (Object.keys(formatting).length === 0) return;
289
290
  return formatting;
290
291
  }