@beyondwork/docx-react-component 1.0.56 → 1.0.57

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 (107) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +157 -0
  3. package/src/compare/diff-engine.ts +3 -0
  4. package/src/core/commands/formatting-commands.ts +1 -0
  5. package/src/core/commands/index.ts +17 -11
  6. package/src/core/selection/mapping.ts +18 -1
  7. package/src/core/selection/review-anchors.ts +29 -18
  8. package/src/io/chart-preview-resolver.ts +175 -41
  9. package/src/io/docx-session.ts +57 -2
  10. package/src/io/export/serialize-main-document.ts +82 -0
  11. package/src/io/export/serialize-styles.ts +61 -3
  12. package/src/io/export/table-properties-xml.ts +19 -4
  13. package/src/io/normalize/normalize-text.ts +33 -0
  14. package/src/io/ooxml/parse-anchor.ts +182 -0
  15. package/src/io/ooxml/parse-drawing.ts +319 -0
  16. package/src/io/ooxml/parse-fields.ts +115 -2
  17. package/src/io/ooxml/parse-fill.ts +215 -0
  18. package/src/io/ooxml/parse-font-table.ts +190 -0
  19. package/src/io/ooxml/parse-footnotes.ts +52 -1
  20. package/src/io/ooxml/parse-main-document.ts +241 -1
  21. package/src/io/ooxml/parse-numbering.ts +96 -0
  22. package/src/io/ooxml/parse-picture.ts +107 -0
  23. package/src/io/ooxml/parse-settings.ts +34 -0
  24. package/src/io/ooxml/parse-shapes.ts +87 -0
  25. package/src/io/ooxml/parse-solid-fill.ts +11 -0
  26. package/src/io/ooxml/parse-styles.ts +74 -1
  27. package/src/io/ooxml/parse-theme.ts +60 -0
  28. package/src/io/paste/html-clipboard.ts +449 -0
  29. package/src/io/paste/word-clipboard.ts +5 -1
  30. package/src/legal/_document-root.ts +26 -0
  31. package/src/legal/bookmarks.ts +4 -3
  32. package/src/legal/cross-references.ts +3 -2
  33. package/src/legal/defined-terms.ts +2 -1
  34. package/src/legal/signature-blocks.ts +2 -1
  35. package/src/model/canonical-document.ts +415 -3
  36. package/src/runtime/chart/chart-model-store.ts +73 -10
  37. package/src/runtime/document-runtime.ts +693 -41
  38. package/src/runtime/edit-ops/index.ts +129 -0
  39. package/src/runtime/event-refresh-hints.ts +7 -0
  40. package/src/runtime/field-resolver.ts +341 -0
  41. package/src/runtime/footnote-resolver.ts +55 -0
  42. package/src/runtime/hyperlink-color-resolver.ts +13 -10
  43. package/src/runtime/object-grab/index.ts +51 -0
  44. package/src/runtime/paragraph-style-resolver.ts +105 -0
  45. package/src/runtime/resolved-numbering-geometry.ts +12 -0
  46. package/src/runtime/selection/cursor-ops.ts +186 -15
  47. package/src/runtime/selection/index.ts +17 -1
  48. package/src/runtime/structure-ops/index.ts +77 -0
  49. package/src/runtime/styles-cascade.ts +33 -0
  50. package/src/runtime/surface-projection.ts +186 -12
  51. package/src/runtime/theme-color-resolver.ts +189 -44
  52. package/src/runtime/units.ts +46 -0
  53. package/src/runtime/view-state.ts +13 -2
  54. package/src/ui/WordReviewEditor.tsx +168 -10
  55. package/src/ui/editor-runtime-boundary.ts +94 -1
  56. package/src/ui/editor-shell-view.tsx +1 -1
  57. package/src/ui/runtime-shortcut-dispatch.ts +17 -3
  58. package/src/ui-tailwind/chart/ChartSurface.tsx +36 -10
  59. package/src/ui-tailwind/chart/layout/plot-area.ts +120 -45
  60. package/src/ui-tailwind/chart/render/area.tsx +22 -4
  61. package/src/ui-tailwind/chart/render/bar-column.tsx +37 -11
  62. package/src/ui-tailwind/chart/render/bubble.tsx +6 -2
  63. package/src/ui-tailwind/chart/render/combo.tsx +37 -4
  64. package/src/ui-tailwind/chart/render/line.tsx +28 -5
  65. package/src/ui-tailwind/chart/render/pie.tsx +36 -16
  66. package/src/ui-tailwind/chart/render/progressive-render.ts +8 -1
  67. package/src/ui-tailwind/chart/render/scatter.tsx +9 -4
  68. package/src/ui-tailwind/chrome/avatar-initials.ts +15 -0
  69. package/src/ui-tailwind/chrome/tw-comment-preview.tsx +3 -1
  70. package/src/ui-tailwind/chrome/tw-context-menu.tsx +14 -0
  71. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +3 -2
  72. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +30 -11
  73. package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +15 -2
  74. package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +1 -1
  75. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +24 -7
  76. package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +31 -12
  77. package/src/ui-tailwind/chrome-overlay/page-border-resolver.ts +211 -0
  78. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -0
  79. package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +74 -0
  80. package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +65 -0
  81. package/src/ui-tailwind/chrome-overlay/tw-page-border-overlay.tsx +233 -0
  82. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +135 -13
  83. package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +51 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +12 -4
  85. package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +32 -12
  86. package/src/ui-tailwind/chrome-overlay/tw-toc-outline-sidebar.tsx +133 -0
  87. package/src/ui-tailwind/editor-surface/chart-node-view.tsx +49 -10
  88. package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +119 -0
  89. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +236 -9
  90. package/src/ui-tailwind/editor-surface/pm-schema.ts +188 -11
  91. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +28 -2
  92. package/src/ui-tailwind/editor-surface/shape-renderer.ts +206 -0
  93. package/src/ui-tailwind/editor-surface/surface-layer.ts +66 -0
  94. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +29 -0
  95. package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +7 -1
  96. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +22 -6
  97. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +10 -16
  98. package/src/ui-tailwind/review/tw-health-panel.tsx +0 -25
  99. package/src/ui-tailwind/review/tw-rail-card.tsx +38 -17
  100. package/src/ui-tailwind/review/tw-review-rail.tsx +2 -2
  101. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +5 -12
  102. package/src/ui-tailwind/review/tw-workflow-tab.tsx +2 -2
  103. package/src/ui-tailwind/theme/editor-theme.css +1 -0
  104. package/src/ui-tailwind/theme/tokens.css +6 -0
  105. package/src/ui-tailwind/theme/tokens.ts +10 -0
  106. package/src/validation/compatibility-engine.ts +2 -0
  107. package/src/validation/docx-comment-proof.ts +12 -3
@@ -1,8 +1,23 @@
1
1
  import type {
2
2
  CompatSetting,
3
+ ClrSchemeMapping,
4
+ ClrSchemeMappingSlot,
3
5
  DocumentSettings,
6
+ ThemeColorSlot,
4
7
  } from "../../model/canonical-document.ts";
5
8
 
9
+ const CLRSCHEME_MAPPING_SLOTS = new Set<string>([
10
+ "bg1", "bg2", "t1", "t2",
11
+ "accent1", "accent2", "accent3", "accent4", "accent5", "accent6",
12
+ "hlink", "followedHyperlink",
13
+ ]);
14
+
15
+ const THEME_COLOR_SLOTS = new Set<string>([
16
+ "dk1", "lt1", "dk2", "lt2",
17
+ "accent1", "accent2", "accent3", "accent4", "accent5", "accent6",
18
+ "hlink", "folHlink",
19
+ ]);
20
+
6
21
  interface XmlElementNode {
7
22
  type: "element";
8
23
  name: string;
@@ -34,6 +49,7 @@ export function parseSettingsXml(xml: string): DocumentSettings {
34
49
  const compatPartition = compat ? partitionCompat(compat) : undefined;
35
50
  const rootCompatFlags = readRootCompatFlags(settingsElement);
36
51
  const themeFontLangElement = findChildElementOptional(settingsElement, "themeFontLang");
52
+ const clrSchemeMapping = parseClrSchemeMapping(settingsElement);
37
53
  const unmodelled = readUnmodelledSettingsChildren(settingsElement);
38
54
 
39
55
  return {
@@ -53,6 +69,7 @@ export function parseSettingsXml(xml: string): DocumentSettings {
53
69
  ...(themeFontLangElement
54
70
  ? { themeFontLang: { ...themeFontLangElement.attributes } }
55
71
  : {}),
72
+ ...(clrSchemeMapping !== undefined ? { clrSchemeMapping } : {}),
56
73
  ...(unmodelled.length > 0 ? { unmodelledSettingsChildren: unmodelled } : {}),
57
74
  };
58
75
  }
@@ -67,6 +84,7 @@ const MODELLED_SETTINGS_CHILD_NAMES = new Set<string>([
67
84
  "zoom",
68
85
  "compat",
69
86
  "themeFontLang",
87
+ "clrSchemeMapping",
70
88
  ]);
71
89
 
72
90
  function readUnmodelledSettingsChildren(
@@ -134,6 +152,22 @@ function partitionCompat(compatElement: XmlElementNode): CompatPartition {
134
152
  return { compatSettings, compatFlags };
135
153
  }
136
154
 
155
+ function parseClrSchemeMapping(
156
+ settingsElement: XmlElementNode,
157
+ ): ClrSchemeMapping | undefined {
158
+ const el = findChildElementOptional(settingsElement, "clrSchemeMapping");
159
+ if (!el) return undefined;
160
+ const mapping: Partial<Record<ClrSchemeMappingSlot, ThemeColorSlot>> = {};
161
+ for (const [attr, value] of Object.entries(el.attributes)) {
162
+ if (!value) continue;
163
+ const local = localName(attr);
164
+ if (!CLRSCHEME_MAPPING_SLOTS.has(local)) continue;
165
+ if (!THEME_COLOR_SLOTS.has(value)) continue;
166
+ mapping[local as ClrSchemeMappingSlot] = value as ThemeColorSlot;
167
+ }
168
+ return Object.keys(mapping).length > 0 ? mapping : undefined;
169
+ }
170
+
137
171
  function findChildElementOptional(
138
172
  node: XmlElementNode,
139
173
  childLocalName: string,
@@ -10,6 +10,9 @@
10
10
  * preserved in the canonical node's rawXml field for lossless round-trip export.
11
11
  */
12
12
 
13
+ import type { ShapeContent } from "../../model/canonical-document.ts";
14
+ import { parseFill } from "./parse-fill.ts";
15
+
13
16
  const WPS_SHAPE_GRAPHIC_URI =
14
17
  "http://schemas.microsoft.com/office/word/2010/wordprocessingShape";
15
18
 
@@ -294,3 +297,87 @@ function findTagEnd(xml: string, start: number): number {
294
297
  }
295
298
  return xml.length - 1;
296
299
  }
300
+
301
+ // ───────────────────────────────────────────────────────────────────────────
302
+ // CO4.3 — parseShapeContent: wps:wsp → ShapeContent (geometry, fill, line,
303
+ // txbxContentXml, optional recursive txbxBlocks).
304
+ // ───────────────────────────────────────────────────────────────────────────
305
+
306
+ export type TxbxBlockParser = (xml: string) => ReadonlyArray<{ type: string; [key: string]: unknown }>;
307
+
308
+ export function parseShapeContent(
309
+ graphicDataEl: XmlElementNode,
310
+ drawingRawXml: string,
311
+ blockParser?: TxbxBlockParser,
312
+ ): ShapeContent | null {
313
+ const uri = graphicDataEl.attributes.uri ?? "";
314
+ if (uri !== WPS_SHAPE_GRAPHIC_URI) return null;
315
+
316
+ const wsp = findFirstDescendant(graphicDataEl, "wsp");
317
+ if (!wsp) return null;
318
+
319
+ const spPr = findFirstChild(wsp, "spPr");
320
+ const prstGeom = spPr ? findFirstChild(spPr, "prstGeom") : undefined;
321
+ const geometry = prstGeom?.attributes.prst;
322
+
323
+ const fill = spPr ? readFill(spPr) : undefined;
324
+ const line = spPr ? readLine(spPr) : undefined;
325
+
326
+ // Text-box content — preserve raw XML for serialization + recurse via the
327
+ // optional blockParser callback (CO4 F3.3) to populate txbxBlocks.
328
+ const txbx = findFirstChild(wsp, "txbx");
329
+ const txbxContent = txbx ? findFirstDescendant(txbx, "txbxContent") : undefined;
330
+ const txbxContentXml = txbxContent ? extractRawXml(txbxContent, drawingRawXml) : undefined;
331
+
332
+ let txbxBlocks: ReadonlyArray<{ type: string; [key: string]: unknown }> | undefined;
333
+ if (txbxContentXml && blockParser) {
334
+ try {
335
+ txbxBlocks = blockParser(txbxContentXml);
336
+ } catch {
337
+ // Preserve-only fallback: keep txbxContentXml for serialization; leave
338
+ // txbxBlocks undefined so consumers know recursion did not succeed.
339
+ txbxBlocks = undefined;
340
+ }
341
+ }
342
+
343
+ // WordArt heuristic: geometry starting with "text" is a WordArt preset —
344
+ // those flow through parseShapeXml/WordArtNode. parseShapeContent covers
345
+ // rectangular shapes + text boxes.
346
+ const isTextBox = Boolean(txbxContent && (!geometry || geometry === "rect"));
347
+
348
+ const result: ShapeContent = { type: "shape", rawXml: drawingRawXml };
349
+ if (geometry) result.geometry = geometry;
350
+ if (fill) result.fill = fill;
351
+ if (line) result.line = line;
352
+ if (isTextBox) result.isTextBox = true;
353
+ if (txbxContentXml) result.txbxContentXml = txbxContentXml;
354
+ if (txbxBlocks && txbxBlocks.length > 0) result.txbxBlocks = txbxBlocks;
355
+ return result;
356
+ }
357
+
358
+ // F3.4 + P5 — readFill delegates to the shared `parseFill` primitive covering
359
+ // solid / none / gradient / pattern. Lane 5 chart-style cascade can consume
360
+ // the same parser via `src/io/ooxml/parse-fill.ts`.
361
+ function readFill(spPr: XmlElementNode): ShapeContent["fill"] {
362
+ return parseFill(spPr);
363
+ }
364
+
365
+ function readLine(
366
+ spPr: XmlElementNode,
367
+ ): { color?: string; widthEmu?: number; noLine?: boolean } | undefined {
368
+ const ln = findFirstChild(spPr, "ln");
369
+ if (!ln) return undefined;
370
+ const result: { color?: string; widthEmu?: number; noLine?: boolean } = {};
371
+ const wRaw = ln.attributes.w;
372
+ if (wRaw !== undefined) {
373
+ const w = parseInt(wRaw, 10);
374
+ if (Number.isFinite(w)) result.widthEmu = w;
375
+ }
376
+ if (findFirstChild(ln, "noFill")) result.noLine = true;
377
+ const solid = findFirstChild(ln, "solidFill");
378
+ if (solid) {
379
+ const srgb = findFirstChild(solid, "srgbClr");
380
+ if (srgb?.attributes.val) result.color = srgb.attributes.val.toUpperCase();
381
+ }
382
+ return Object.keys(result).length > 0 ? result : undefined;
383
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @deprecated Prefer `parseFill` from `./parse-fill.ts` which covers
3
+ * solid + none + gradient + pattern fills. This file retains the
4
+ * `parseSolidFill` / `SolidFillResult` exports for back-compat with
5
+ * external consumers that only need the solid-fill subset.
6
+ *
7
+ * CO4 P5: `parseFill` is the canonical entry; `parse-shapes.ts` already
8
+ * migrated. Remove this shim once no external imports reference it.
9
+ */
10
+
11
+ export { parseSolidFill, type SolidFillResult, type XmlElementNode } from "./parse-fill.ts";
@@ -12,6 +12,7 @@ import type {
12
12
  CharacterStyleDefinition,
13
13
  DocumentDefaults,
14
14
  LatentStyleDefinition,
15
+ NumberingStyleDefinition,
15
16
  ParagraphStyleDefinition,
16
17
  StylesCatalog,
17
18
  TableBorders,
@@ -118,6 +119,7 @@ export function parseStylesXml(xml: string): ParseStylesResult {
118
119
  const paragraphs: Record<string, ParagraphStyleDefinition> = {};
119
120
  const characters: Record<string, CharacterStyleDefinition> = {};
120
121
  const tables: Record<string, TableStyleDefinition> = {};
122
+ const numberingStyles: Record<string, NumberingStyleDefinition> = {};
121
123
  const latentStyles: Record<string, LatentStyleDefinition> = {};
122
124
  let docDefaults: DocumentDefaults | undefined;
123
125
 
@@ -147,6 +149,7 @@ export function parseStylesXml(xml: string): ParseStylesResult {
147
149
  if (!styleId) continue;
148
150
 
149
151
  const displayName = readStyleDisplayName(child) ?? styleId;
152
+ const aliases = readStyleAliases(child);
150
153
  const basedOn = readLinkedStyleId(child, "basedOn");
151
154
  const isDefault = (child.attributes["w:default"] ?? child.attributes.default) === "1";
152
155
 
@@ -154,6 +157,7 @@ export function parseStylesXml(xml: string): ParseStylesResult {
154
157
  case "paragraph": {
155
158
  const nextStyle = readLinkedStyleId(child, "next");
156
159
  const linkedStyleId = readLinkedStyleId(child, "link");
160
+ const autoRedefine = readStyleOnOff(child, "autoRedefine");
157
161
  const outlineLevel = readParagraphStyleOutlineLevel(child);
158
162
  const numbering = readParagraphStyleNumbering(child);
159
163
  const pPrNode = findChildElementOptional(child, "pPr");
@@ -165,8 +169,10 @@ export function parseStylesXml(xml: string): ParseStylesResult {
165
169
  displayName,
166
170
  kind: "paragraph",
167
171
  isDefault,
172
+ ...(aliases ? { aliases } : {}),
168
173
  ...(basedOn ? { basedOn } : {}),
169
174
  ...(nextStyle ? { nextStyle } : {}),
175
+ ...(autoRedefine !== undefined ? { autoRedefine } : {}),
170
176
  ...(outlineLevel !== undefined ? { outlineLevel } : {}),
171
177
  ...(numbering ? { numbering } : {}),
172
178
  ...(paragraphProperties ? { paragraphProperties } : {}),
@@ -184,6 +190,7 @@ export function parseStylesXml(xml: string): ParseStylesResult {
184
190
  displayName,
185
191
  kind: "character",
186
192
  isDefault,
193
+ ...(aliases ? { aliases } : {}),
187
194
  ...(basedOn ? { basedOn } : {}),
188
195
  ...(runProperties ? { runProperties } : {}),
189
196
  ...(linkedStyleId ? { linkedStyleId } : {}),
@@ -198,14 +205,37 @@ export function parseStylesXml(xml: string): ParseStylesResult {
198
205
  displayName,
199
206
  kind: "table",
200
207
  isDefault,
208
+ ...(aliases ? { aliases } : {}),
201
209
  ...(basedOn ? { basedOn } : {}),
202
210
  ...(formatting ? { formatting } : {}),
203
211
  ...(conditionalFormatting ? { conditionalFormatting } : {}),
204
212
  };
205
213
  break;
206
214
  }
215
+ case "numbering": {
216
+ // Preserve-only: stash numId reference if present; no property body.
217
+ const pPrNode = findChildElementOptional(child, "pPr");
218
+ const numPrNode = pPrNode ? findChildElementOptional(pPrNode, "numPr") : undefined;
219
+ const numIdNode = numPrNode ? findChildElementOptional(numPrNode, "numId") : undefined;
220
+ const rawNumId = numIdNode
221
+ ? (numIdNode.attributes["w:val"] ?? numIdNode.attributes.val)
222
+ : undefined;
223
+ const numberingInstanceId = rawNumId
224
+ ? toCanonicalNumberingInstanceId(rawNumId)
225
+ : undefined;
226
+ numberingStyles[styleId] = {
227
+ styleId,
228
+ displayName,
229
+ kind: "numbering",
230
+ isDefault,
231
+ ...(basedOn ? { basedOn } : {}),
232
+ ...(aliases ? { aliases } : {}),
233
+ ...(numberingInstanceId ? { numberingInstanceId } : {}),
234
+ };
235
+ break;
236
+ }
207
237
  default:
208
- // numbering/list styles are not part of the canonical catalog
238
+ // Unknown w:type skip. Valid Word outputs paragraph/character/table/numbering only.
209
239
  break;
210
240
  }
211
241
  } else if (local === "latentStyles") {
@@ -216,10 +246,12 @@ export function parseStylesXml(xml: string): ParseStylesResult {
216
246
  resolveStyleLinkReciprocals(paragraphs, characters, diagnostics);
217
247
 
218
248
  const hasLatent = Object.keys(latentStyles).length > 0;
249
+ const hasNumberingStyles = Object.keys(numberingStyles).length > 0;
219
250
  diagnostics.push(
220
251
  `parsed ${Object.keys(paragraphs).length} paragraph, ` +
221
252
  `${Object.keys(characters).length} character, ` +
222
253
  `${Object.keys(tables).length} table styles` +
254
+ (hasNumberingStyles ? `, ${Object.keys(numberingStyles).length} numbering styles` : "") +
223
255
  (hasLatent ? `, ${Object.keys(latentStyles).length} latent styles` : ""),
224
256
  );
225
257
 
@@ -228,6 +260,7 @@ export function parseStylesXml(xml: string): ParseStylesResult {
228
260
  paragraphs,
229
261
  characters,
230
262
  tables,
263
+ ...(hasNumberingStyles ? { numberingStyles } : {}),
231
264
  ...(hasLatent ? { latentStyles } : {}),
232
265
  fromPackage: true,
233
266
  ...(docDefaults ? { docDefaults } : {}),
@@ -247,6 +280,39 @@ function readStyleDisplayName(styleNode: XmlElementNode): string | undefined {
247
280
  return nameEl.attributes["w:val"] ?? nameEl.attributes.val ?? undefined;
248
281
  }
249
282
 
283
+ /**
284
+ * ST_OnOff reader matching `src/io/ooxml/xml-attr-helpers.ts readOnOff`:
285
+ * missing → undefined; bare `<w:foo/>` or w:val="1|true|on" → true;
286
+ * w:val="0|false|off" → false. Duplicated locally because parse-styles.ts
287
+ * uses its own inline XmlElementNode type.
288
+ */
289
+ function readStyleOnOff(styleNode: XmlElementNode, elementLocalName: string): boolean | undefined {
290
+ const el = findChildElementOptional(styleNode, elementLocalName);
291
+ if (!el) return undefined;
292
+ const raw = el.attributes["w:val"] ?? el.attributes.val;
293
+ if (raw === undefined) return true;
294
+ const n = raw.toLowerCase();
295
+ if (n === "0" || n === "false" || n === "off") return false;
296
+ return true;
297
+ }
298
+
299
+ /**
300
+ * Read `<w:aliases w:val="A,B,C"/>` — ECMA-376 §17.7.4.2.
301
+ * Returns undefined when the element is missing or carries no values.
302
+ * Whitespace around individual entries is trimmed; empty entries are dropped.
303
+ */
304
+ function readStyleAliases(styleNode: XmlElementNode): string[] | undefined {
305
+ const el = findChildElementOptional(styleNode, "aliases");
306
+ if (!el) return undefined;
307
+ const raw = el.attributes["w:val"] ?? el.attributes.val;
308
+ if (!raw) return undefined;
309
+ const parts = raw
310
+ .split(",")
311
+ .map((s) => s.trim())
312
+ .filter((s) => s.length > 0);
313
+ return parts.length > 0 ? parts : undefined;
314
+ }
315
+
250
316
  function readLinkedStyleId(
251
317
  styleNode: XmlElementNode,
252
318
  elementLocalName: string,
@@ -369,8 +435,15 @@ function readTableStyleFormatting(styleNode: XmlElementNode): TableStyleFormatti
369
435
  const tableProperties = findChildElementOptional(styleNode, "tblPr");
370
436
  const rowProperties = findChildElementOptional(styleNode, "trPr");
371
437
  const cellProperties = findChildElementOptional(styleNode, "tcPr");
438
+ const pPrNode = findChildElementOptional(styleNode, "pPr");
439
+ const rPrNode = findChildElementOptional(styleNode, "rPr");
372
440
  const formatting: TableStyleFormatting = {};
373
441
 
442
+ const paragraphProperties = readParagraphProperties(pPrNode);
443
+ if (paragraphProperties) formatting.paragraphProperties = paragraphProperties;
444
+ const runProperties = readRunProperties(rPrNode);
445
+ if (runProperties) formatting.runProperties = runProperties;
446
+
374
447
  if (tableProperties) {
375
448
  const table: NonNullable<TableStyleFormatting["table"]> = {};
376
449
  const width = readTableWidth(tableProperties);
@@ -3,6 +3,8 @@ import type {
3
3
  ThemeDefinition,
4
4
  ThemeFontScheme,
5
5
  ResolvedTheme,
6
+ CanonicalTheme,
7
+ ClrSchemeMapping,
6
8
  } from "../../model/canonical-document.ts";
7
9
  import type { XmlElementNode } from "./xml-element.ts";
8
10
  import { parseXml } from "./xml-parser.ts";
@@ -103,8 +105,66 @@ export function resolveThemeColor(
103
105
  return theme?.colors[colorSlot];
104
106
  }
105
107
 
108
+ /**
109
+ * Default clrSchemeMapping per ECMA-376 §17.15.1.17.
110
+ *
111
+ * Word applies this identity when `settings.xml` omits `<w:clrSchemeMapping>`.
112
+ * Without it, style slots like `t1` never reach a physical theme color and
113
+ * `resolveAuto()` (→ `t1`) returns undefined on every document that doesn't
114
+ * ship an explicit mapping — which is most of them. See Lane CO1 follow-up.
115
+ */
116
+ export const DEFAULT_CLR_SCHEME_MAPPING: ClrSchemeMapping = Object.freeze({
117
+ bg1: "lt1",
118
+ t1: "dk1",
119
+ bg2: "lt2",
120
+ t2: "dk2",
121
+ accent1: "accent1",
122
+ accent2: "accent2",
123
+ accent3: "accent3",
124
+ accent4: "accent4",
125
+ accent5: "accent5",
126
+ accent6: "accent6",
127
+ hlink: "hlink",
128
+ followedHyperlink: "folHlink",
129
+ });
130
+
131
+ /**
132
+ * Combine a parsed `ThemeDefinition` with a `ClrSchemeMapping` from
133
+ * settings.xml into a `CanonicalTheme` ready for runtime resolution.
134
+ *
135
+ * Any style slot absent from `clrMap` falls back to `DEFAULT_CLR_SCHEME_MAPPING`
136
+ * (ECMA-376 §17.15.1.17 default) so documents without `<w:clrSchemeMapping>`
137
+ * still resolve `t1 → dk1` etc.
138
+ *
139
+ * The `themeHash` and `clrMapHash` fields are deterministic content hashes
140
+ * (sorted key:value concatenation) suitable as structural cache keys per
141
+ * CLAUDE.md §3.
142
+ */
143
+ export function materializeCanonicalTheme(
144
+ theme: ThemeDefinition,
145
+ clrMap: ClrSchemeMapping,
146
+ ): CanonicalTheme {
147
+ const clrScheme: ThemeColorScheme = theme.colorScheme ?? { name: "", colors: {} };
148
+ const mergedClrMap: ClrSchemeMapping = { ...DEFAULT_CLR_SCHEME_MAPPING, ...clrMap };
149
+ return {
150
+ clrScheme,
151
+ ...(theme.fontScheme !== undefined ? { fontScheme: theme.fontScheme } : {}),
152
+ clrMap: mergedClrMap,
153
+ themeHash: hashRecord(clrScheme.colors),
154
+ clrMapHash: hashRecord(mergedClrMap),
155
+ };
156
+ }
157
+
106
158
  // ---- Internal helpers ----
107
159
 
160
+ function hashRecord(rec: Partial<Record<string, string>>): string {
161
+ return Object.entries(rec)
162
+ .filter((entry): entry is [string, string] => entry[1] !== undefined)
163
+ .sort(([a], [b]) => a.localeCompare(b))
164
+ .map(([k, v]) => `${k}:${v}`)
165
+ .join("|");
166
+ }
167
+
108
168
  function parseColorScheme(
109
169
  element: XmlElementNode | undefined,
110
170
  ): ThemeColorScheme | undefined {