@stll/folio-core 0.22.3 → 0.23.1

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 (78) hide show
  1. package/dist/ai-edits/apply.d.ts +1 -1
  2. package/dist/controller/layoutPipeline.js +7 -3
  3. package/dist/document-operations.d.ts +4 -4
  4. package/dist/document-operations.js +17 -1
  5. package/dist/docx/blockContentParser.js +2 -47
  6. package/dist/docx/fieldParser.d.ts +1 -1
  7. package/dist/docx/fieldParser.js +12 -4
  8. package/dist/docx/numberingParser.d.ts +2 -11
  9. package/dist/docx/numberingParser.js +3 -79
  10. package/dist/docx/ooxmlCounterFormatter.d.ts +8 -0
  11. package/dist/docx/ooxmlCounterFormatter.js +134 -0
  12. package/dist/docx/rezip.d.ts +1 -1
  13. package/dist/docx/rezip.js +12 -31
  14. package/dist/docx/serializer/tableSerializer.js +1 -1
  15. package/dist/docx/tableParser.js +2 -1
  16. package/dist/fields/bookmarkPages.js +1 -1
  17. package/dist/fields/evaluateField.js +1 -1
  18. package/dist/fields/fieldContext.d.ts +2 -0
  19. package/dist/fields/resolveFieldValues.js +5 -2
  20. package/dist/layout-bridge/convert/headerFooterLayout.js +6 -0
  21. package/dist/layout-bridge/convert/toFlowBlocks.js +51 -109
  22. package/dist/layout-engine/index.d.ts +3 -2
  23. package/dist/layout-engine/index.js +40 -16
  24. package/dist/layout-engine/measure/cache.js +1 -1
  25. package/dist/layout-engine/measure/complexScriptFormatting.d.ts +18 -0
  26. package/dist/layout-engine/measure/complexScriptFormatting.js +11 -0
  27. package/dist/layout-engine/measure/measureContainer.js +31 -16
  28. package/dist/layout-engine/measure/measureHelpers.js +4 -0
  29. package/dist/layout-engine/measure/measureParagraph.js +13 -3
  30. package/dist/layout-engine/measure/measureTypes.d.ts +4 -0
  31. package/dist/layout-engine/paginator.d.ts +5 -3
  32. package/dist/layout-engine/paginator.js +34 -13
  33. package/dist/layout-engine/types.d.ts +31 -4
  34. package/dist/layout-painter/imageLayout.d.ts +1 -1
  35. package/dist/layout-painter/index.js +2 -1
  36. package/dist/layout-painter/renderPage.js +9 -3
  37. package/dist/layout-painter/renderParagraph.js +42 -25
  38. package/dist/layout-painter/renderUtils.d.ts +2 -0
  39. package/dist/managers/AutoSaveManager.js +8 -40
  40. package/dist/managers/TableSelectionManager.d.ts +1 -1
  41. package/dist/managers/autoSaveCodec.d.ts +17 -0
  42. package/dist/managers/autoSaveCodec.js +109 -0
  43. package/dist/paged-layout/sectionGeometry.d.ts +3 -2
  44. package/dist/paged-layout/sectionGeometry.js +17 -1
  45. package/dist/prosemirror/attrs/index.js +278 -25
  46. package/dist/prosemirror/commands/comments.js +3 -3
  47. package/dist/prosemirror/commands/formatting.js +19 -19
  48. package/dist/prosemirror/commands/index.d.ts +2 -2
  49. package/dist/prosemirror/commands/paragraph.js +32 -32
  50. package/dist/prosemirror/commands/propertyChangeScope.d.ts +4 -2
  51. package/dist/prosemirror/commands/propertyChangeScope.js +8 -8
  52. package/dist/prosemirror/commands/table.d.ts +10 -52
  53. package/dist/prosemirror/commands/table.js +34 -34
  54. package/dist/prosemirror/conversion/fromProseDoc.js +24 -33
  55. package/dist/prosemirror/conversion/sdtAttrs.d.ts +2 -1
  56. package/dist/prosemirror/conversion/sdtAttrs.js +9 -4
  57. package/dist/prosemirror/conversion/toProseDoc.js +4 -1
  58. package/dist/prosemirror/extensions/ExtensionManager.d.ts +6 -3
  59. package/dist/prosemirror/extensions/ExtensionManager.js +5 -3
  60. package/dist/prosemirror/extensions/core/ParagraphExtension.d.ts +1 -1
  61. package/dist/prosemirror/extensions/features/ListExtension.js +4 -4
  62. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.d.ts +3 -2
  63. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +31 -17
  64. package/dist/prosemirror/extensions/nodes/ShapeExtension.d.ts +1 -1
  65. package/dist/prosemirror/extensions/nodes/TableExtension.js +1 -0
  66. package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +1 -1
  67. package/dist/prosemirror/extensions/types.d.ts +134 -1
  68. package/dist/prosemirror/index.d.ts +2 -2
  69. package/dist/prosemirror/plugins/selectionTracker.js +25 -99
  70. package/dist/prosemirror/revisionCarriers.js +3 -1
  71. package/dist/prosemirror/schema/index.d.ts +2 -2
  72. package/dist/prosemirror/schema/index.js +87 -0
  73. package/dist/prosemirror/schema/marks.d.ts +8 -0
  74. package/dist/prosemirror/schema/nodes.d.ts +15 -2
  75. package/dist/prosemirror/selectionState.d.ts +11 -5
  76. package/dist/prosemirror/selectionState.js +94 -57
  77. package/dist/utils/tableOperations.d.ts +7 -6
  78. package/package.json +3 -3
@@ -17,7 +17,7 @@ type FolioAIEditView = {
17
17
  type ApplyFolioAIEditOperationsOptions = {
18
18
  view: FolioAIEditView;
19
19
  snapshot: FolioAIEditSnapshot;
20
- operations: FolioAIEditOperation[];
20
+ operations: readonly FolioAIEditOperation[];
21
21
  mode?: FolioAIEditApplyMode;
22
22
  author?: string;
23
23
  /** Optional author initials (w:initials) stamped alongside the author. */
@@ -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;
@@ -56,10 +56,10 @@ declare class InvalidFolioDocumentOperationBatchError extends InvalidFolioDocume
56
56
  declare const assertSupportedFolioDocumentOperationVersion: (value: unknown) => typeof FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION;
57
57
  type FolioDocumentOperationBatch = {
58
58
  readonly version: typeof FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION;
59
- operations: FolioDocumentOperation[];
60
- mode?: FolioDocumentOperationMode;
61
- atomic?: boolean;
62
- dryRun?: boolean;
59
+ readonly operations: readonly FolioDocumentOperation[];
60
+ readonly mode?: FolioDocumentOperationMode;
61
+ readonly atomic?: boolean;
62
+ readonly dryRun?: boolean;
63
63
  };
64
64
  declare const parseFolioDocumentOperationBatch: (value: unknown) => FolioDocumentOperationBatch;
65
65
  type FolioDocumentOperationStatus = "committed" | "previewed" | "rejected";
@@ -34,6 +34,7 @@ const FOLIO_DOCUMENT_OPERATION_STORIES = Object.freeze([
34
34
  ]);
35
35
  const FOLIO_DOCUMENT_OPERATION_PRECONDITIONS = Object.freeze(["blockTextHash"]);
36
36
  const FOLIO_DOCUMENT_OPERATION_BATCH_MODES = Object.freeze(["best-effort", "atomic"]);
37
+ const parsedFolioDocumentOperationBatches = /* @__PURE__ */ new WeakSet();
37
38
  const DIRECT_AND_TRACKED_MODES = Object.freeze(["direct", "tracked-changes"]);
38
39
  const DIRECT_TRACKED_AND_SUGGESTED_MODES = FOLIO_DOCUMENT_OPERATION_MODES;
39
40
  const DIRECT_AND_SUGGESTED_MODES = Object.freeze(["direct", "suggested"]);
@@ -83,6 +84,17 @@ const assertSupportedFolioDocumentOperationVersion = (value) => {
83
84
  });
84
85
  };
85
86
  const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
87
+ const isParsedFolioDocumentOperationBatch = (value) => typeof value === "object" && value !== null && parsedFolioDocumentOperationBatches.has(value);
88
+ const freezeParsedValue = (value) => {
89
+ if (Array.isArray(value)) {
90
+ for (const entry of value) freezeParsedValue(entry);
91
+ Object.freeze(value);
92
+ return;
93
+ }
94
+ if (!isPlainObject(value)) return;
95
+ for (const entry of Object.values(value)) freezeParsedValue(entry);
96
+ Object.freeze(value);
97
+ };
86
98
  const invalidBatch = (path, reason) => {
87
99
  throw new InvalidFolioDocumentOperationBatchError({
88
100
  message: `Invalid document operation batch at ${path}: ${reason}.`,
@@ -508,6 +520,7 @@ const parseDocumentOperation = (value, index) => {
508
520
  return invalidBatch(`${path}.type`, `unsupported operation type "${type}"`);
509
521
  };
510
522
  const parseFolioDocumentOperationBatch = (value) => {
523
+ if (isParsedFolioDocumentOperationBatch(value)) return value;
511
524
  if (!isPlainObject(value)) return invalidBatch("$", "expected an object");
512
525
  assertAllowedKeys(value, "$", [
513
526
  "version",
@@ -529,13 +542,16 @@ const parseFolioDocumentOperationBatch = (value) => {
529
542
  if (operationIds.has(operation.id)) return invalidBatch(`$.operations[${index}].id`, "expected a unique operation id");
530
543
  operationIds.add(operation.id);
531
544
  }
532
- return {
545
+ const parsedBatch = {
533
546
  version,
534
547
  operations: parsedOperations,
535
548
  ...mode !== void 0 && { mode },
536
549
  ...atomic !== void 0 && { atomic },
537
550
  ...dryRun !== void 0 && { dryRun }
538
551
  };
552
+ freezeParsedValue(parsedBatch);
553
+ parsedFolioDocumentOperationBatches.add(parsedBatch);
554
+ return parsedBatch;
539
555
  };
540
556
  const recoveryByReason = {
541
557
  missingBlock: "refreshDocument",
@@ -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
@@ -46,16 +47,6 @@ declare function computeListRendering(numPr: {
46
47
  }, numbering: NumberingMap): (document_d_exports.ListRendering & {
47
48
  levelStarts: number[];
48
49
  }) | 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
50
  /**
60
51
  * Compare two `numPr` references by value — `numId` plus `ilvl` (a missing
61
52
  * `ilvl` is level 0). Used for the style-sourced-numbering provenance check;
@@ -89,4 +80,4 @@ declare function getBulletCharacter(level: document_d_exports.ListLevel): string
89
80
  */
90
81
  declare function isBulletLevel(level: document_d_exports.ListLevel): boolean;
91
82
  //#endregion
92
- export { NumberingMap, computeListRendering, createNumberingMap, formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, numPrEqual, padDecimal, parseNumbering, renderListMarker };
83
+ export { NumberingMap, computeListRendering, createNumberingMap, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, numPrEqual, padDecimal, parseNumbering, renderListMarker };
@@ -1,3 +1,4 @@
1
+ import { formatOoxmlCounter, padDecimal } from "./ooxmlCounterFormatter.js";
1
2
  import { FontHintSchema, LevelSuffixSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
2
3
  import { findChild, findChildren, getAttribute, parseBooleanElement, parseNumericAttribute, parseXmlDocument } from "./xmlParser.js";
3
4
  //#region src/docx/numberingParser.ts
@@ -614,37 +615,6 @@ function computeListRendering(numPr, numbering) {
614
615
  return rendering;
615
616
  }
616
617
  /**
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
618
  * Compare two `numPr` references by value — `numId` plus `ilvl` (a missing
649
619
  * `ilvl` is level 0). Used for the style-sourced-numbering provenance check;
650
620
  * avoids JSON.stringify equality, which is sensitive to key order.
@@ -654,52 +624,6 @@ function numPrEqual(a, b) {
654
624
  return a.numId === b.numId && (a.ilvl ?? 0) === (b.ilvl ?? 0);
655
625
  }
656
626
  /**
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
627
  * Render list marker text by replacing placeholders with formatted numbers
704
628
  *
705
629
  * @param lvlText - The level text pattern (e.g., "%1.", "%1.%2")
@@ -713,7 +637,7 @@ function renderListMarker(lvlText, counters, formats) {
713
637
  const placeholder = `%${i}`;
714
638
  if (result.includes(placeholder)) {
715
639
  const counterIndex = i - 1;
716
- const formatted = formatNumber(counters[counterIndex] ?? 1, formats[counterIndex] ?? "decimal");
640
+ const formatted = formatOoxmlCounter(counters[counterIndex] ?? 1, formats[counterIndex] ?? "decimal");
717
641
  result = result.replaceAll(placeholder, formatted);
718
642
  }
719
643
  }
@@ -742,4 +666,4 @@ function isBulletLevel(level) {
742
666
  return level.numFmt === "bullet" || level.numFmt === "none";
743
667
  }
744
668
  //#endregion
745
- export { computeListRendering, createNumberingMap, formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, numPrEqual, padDecimal, parseNumbering, renderListMarker };
669
+ export { computeListRendering, createNumberingMap, formatOoxmlCounter as formatNumber, getBulletCharacter, getCachedNumberingMap, isBulletLevel, 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 };
@@ -145,7 +145,7 @@ declare function updateCoreProperties(corePropsXml: string, { updateModifiedDate
145
145
  * @param buffer - Buffer to validate
146
146
  * @returns Promise resolving to validation result
147
147
  */
148
- declare function validateDocx(buffer: ArrayBuffer): Promise<{
148
+ declare const validateDocx: (buffer: ArrayBuffer) => Promise<{
149
149
  valid: boolean;
150
150
  errors: string[];
151
151
  warnings: string[];
@@ -21,6 +21,7 @@ import { isPreservableDocxEntry } from "./unzip.js";
21
21
  import { WORDPROCESSINGML_NAMESPACE_URIS, findChild, getChildElements, getLocalName, getNamespaceUri, matchesName, parseXml } from "./xmlParser.js";
22
22
  import { assertXmlResourceLimits } from "./xmlResourceLimits.js";
23
23
  import { panic } from "better-result";
24
+ import { validateDocxPackage } from "@stll/docx-core";
24
25
  import JSZip from "jszip";
25
26
  //#region src/docx/rezip.ts
26
27
  /**
@@ -1223,38 +1224,18 @@ function getContentTypeForExtension(extension, mimeType) {
1223
1224
  * @param buffer - Buffer to validate
1224
1225
  * @returns Promise resolving to validation result
1225
1226
  */
1226
- async function validateDocx(buffer) {
1227
- const errors = [];
1228
- const warnings = [];
1229
- try {
1230
- const zip = await JSZip.loadAsync(buffer);
1231
- for (const file of ["[Content_Types].xml", "word/document.xml"]) if (!zip.file(file)) errors.push(`Missing required file: ${file}`);
1232
- for (const file of [
1233
- "_rels/.rels",
1234
- "word/_rels/document.xml.rels",
1235
- "word/styles.xml"
1236
- ]) if (!zip.file(file)) warnings.push(`Missing recommended file: ${file}`);
1237
- const docFile = zip.file("word/document.xml");
1238
- if (docFile) {
1239
- const docXml = await docFile.async("text");
1240
- if (!docXml.includes("<?xml")) warnings.push("document.xml missing XML declaration");
1241
- if (!docXml.includes("<w:document")) errors.push("document.xml missing w:document element");
1242
- if (!docXml.includes("<w:body>")) errors.push("document.xml missing w:body element");
1243
- }
1244
- const ctFile = zip.file("[Content_Types].xml");
1245
- if (ctFile) {
1246
- const ctXml = await ctFile.async("text");
1247
- if (!ctXml.includes("word/document.xml") && !ctXml.includes("application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml")) warnings.push("Content_Types.xml may be missing document.xml type declaration");
1248
- }
1249
- } catch (error) {
1250
- errors.push(`Failed to read as ZIP: ${error instanceof Error ? error.message : "Unknown error"}`);
1251
- }
1252
- return {
1253
- valid: errors.length === 0,
1254
- errors,
1255
- warnings
1227
+ const validateDocx = async (buffer) => {
1228
+ const result = await validateDocxPackage(buffer);
1229
+ return result.valid ? {
1230
+ valid: true,
1231
+ errors: [],
1232
+ warnings: []
1233
+ } : {
1234
+ valid: false,
1235
+ errors: [result.error],
1236
+ warnings: []
1256
1237
  };
1257
- }
1238
+ };
1258
1239
  /**
1259
1240
  * Check if buffer looks like a DOCX file (quick check)
1260
1241
  *
@@ -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
  }
@@ -13,7 +13,7 @@ function buildBookmarkPageMap(pages, blocks) {
13
13
  if (anchors.length === 0) return pageByBookmark;
14
14
  for (const page of pages) for (const fragment of page.fragments) for (const anchor of anchors) {
15
15
  if (!fragmentContainsAnchor(fragment, anchor)) continue;
16
- assignBookmarkPage(pageByBookmark, anchor.names, page.number);
16
+ assignBookmarkPage(pageByBookmark, anchor.names, page.logicalNumber);
17
17
  }
18
18
  return pageByBookmark;
19
19
  }
@@ -38,7 +38,7 @@ function evaluateField(parsed, ctx, options = {}) {
38
38
  const fallback = options.fallback ?? "";
39
39
  if (options.locked) return fallback;
40
40
  switch (parsed.type) {
41
- case "PAGE": return computePageNumber(ctx.pageNumber, parsed);
41
+ case "PAGE": return computePageNumber(ctx.pageNumber, parsed, ctx.pageNumberFormat);
42
42
  case "NUMPAGES": return computePageNumber(ctx.totalPages, parsed);
43
43
  case "SECTIONPAGES": return ctx.sectionPages === void 0 ? fallback : computePageNumber(ctx.sectionPages, parsed);
44
44
  case "TIME": {
@@ -9,6 +9,8 @@
9
9
  type FieldContext = {
10
10
  /** 1-indexed page the field is on (PAGE). */
11
11
  pageNumber: number;
12
+ /** OOXML section-level format used when PAGE has no explicit format switch. */
13
+ pageNumberFormat?: string;
12
14
  /** Total pages in the document (NUMPAGES). */
13
15
  totalPages: number;
14
16
  /** Pages in the field's current section (SECTIONPAGES); omit until the
@@ -22,6 +22,7 @@ function resolveFieldValues(blocks, pages, shared) {
22
22
  const sectionPages = location ? shared.sectionPageCounts.get(location.sectionIndex) : void 0;
23
23
  const context = {
24
24
  pageNumber: location?.page ?? 1,
25
+ ...location?.pageFormat === void 0 ? {} : { pageNumberFormat: location.pageFormat },
25
26
  totalPages: shared.totalPages,
26
27
  bookmarkPages: shared.bookmarkPages,
27
28
  bookmarkText: shared.bookmarkText,
@@ -131,7 +132,8 @@ function fieldValuesEqual(a, b) {
131
132
  function buildBlockPageMap(pages) {
132
133
  const map = /* @__PURE__ */ new Map();
133
134
  for (const page of pages) for (const fragment of page.fragments) if (!map.has(fragment.blockId)) map.set(fragment.blockId, {
134
- page: page.number,
135
+ page: page.logicalNumber,
136
+ ...page.logicalNumberFormat === void 0 ? {} : { pageFormat: page.logicalNumberFormat },
135
137
  sectionIndex: page.sectionIndex ?? 0
136
138
  });
137
139
  return map;
@@ -143,7 +145,8 @@ function buildTableRowPageMap(pages) {
143
145
  for (let rowIndex = fragment.fromRow; rowIndex < fragment.toRow; rowIndex++) {
144
146
  const key = tableRowKey(fragment.blockId, rowIndex);
145
147
  if (!map.has(key)) map.set(key, {
146
- page: page.number,
148
+ page: page.logicalNumber,
149
+ ...page.logicalNumberFormat === void 0 ? {} : { pageFormat: page.logicalNumberFormat },
147
150
  sectionIndex: page.sectionIndex ?? 0
148
151
  });
149
152
  }