@beyondwork/docx-react-component 1.0.56 → 1.0.58
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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/api/public-types.ts +330 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +17 -11
- package/src/core/selection/mapping.ts +18 -1
- package/src/core/selection/review-anchors.ts +29 -18
- package/src/io/chart-preview-resolver.ts +175 -41
- package/src/io/docx-session.ts +57 -2
- package/src/io/export/serialize-main-document.ts +82 -0
- package/src/io/export/serialize-styles.ts +61 -3
- package/src/io/export/table-properties-xml.ts +19 -4
- package/src/io/normalize/normalize-text.ts +33 -0
- package/src/io/ooxml/parse-anchor.ts +182 -0
- package/src/io/ooxml/parse-drawing.ts +319 -0
- package/src/io/ooxml/parse-fields.ts +115 -2
- package/src/io/ooxml/parse-fill.ts +215 -0
- package/src/io/ooxml/parse-font-table.ts +190 -0
- package/src/io/ooxml/parse-footnotes.ts +52 -1
- package/src/io/ooxml/parse-main-document.ts +241 -1
- package/src/io/ooxml/parse-numbering.ts +96 -0
- package/src/io/ooxml/parse-picture.ts +158 -0
- package/src/io/ooxml/parse-settings.ts +34 -0
- package/src/io/ooxml/parse-shapes.ts +87 -0
- package/src/io/ooxml/parse-solid-fill.ts +11 -0
- package/src/io/ooxml/parse-styles.ts +74 -1
- package/src/io/ooxml/parse-theme.ts +60 -0
- package/src/io/paste/html-clipboard.ts +449 -0
- package/src/io/paste/word-clipboard.ts +5 -1
- package/src/legal/_document-root.ts +26 -0
- package/src/legal/bookmarks.ts +4 -3
- package/src/legal/cross-references.ts +3 -2
- package/src/legal/defined-terms.ts +2 -1
- package/src/legal/signature-blocks.ts +2 -1
- package/src/model/canonical-document.ts +421 -3
- package/src/runtime/chart/chart-model-store.ts +73 -10
- package/src/runtime/document-runtime.ts +760 -41
- package/src/runtime/document-search.ts +61 -0
- package/src/runtime/edit-ops/index.ts +129 -0
- package/src/runtime/event-refresh-hints.ts +7 -0
- package/src/runtime/field-resolver.ts +341 -0
- package/src/runtime/footnote-resolver.ts +55 -0
- package/src/runtime/hyperlink-color-resolver.ts +13 -10
- package/src/runtime/object-grab/index.ts +51 -0
- package/src/runtime/paragraph-style-resolver.ts +105 -0
- package/src/runtime/query-scopes.ts +186 -0
- package/src/runtime/resolved-numbering-geometry.ts +12 -0
- package/src/runtime/scope-resolver.ts +60 -0
- package/src/runtime/selection/cursor-ops.ts +186 -15
- package/src/runtime/selection/index.ts +17 -1
- package/src/runtime/structure-ops/index.ts +77 -0
- package/src/runtime/styles-cascade.ts +33 -0
- package/src/runtime/surface-projection.ts +192 -12
- package/src/runtime/theme-color-resolver.ts +189 -44
- package/src/runtime/units.ts +46 -0
- package/src/runtime/view-state.ts +13 -2
- package/src/ui/WordReviewEditor.tsx +239 -11
- package/src/ui/editor-runtime-boundary.ts +97 -1
- package/src/ui/editor-shell-view.tsx +1 -1
- package/src/ui/runtime-shortcut-dispatch.ts +17 -3
- package/src/ui-tailwind/chart/ChartSurface.tsx +36 -10
- package/src/ui-tailwind/chart/layout/plot-area.ts +120 -45
- package/src/ui-tailwind/chart/render/area.tsx +22 -4
- package/src/ui-tailwind/chart/render/bar-column.tsx +37 -11
- package/src/ui-tailwind/chart/render/bubble.tsx +6 -2
- package/src/ui-tailwind/chart/render/combo.tsx +37 -4
- package/src/ui-tailwind/chart/render/line.tsx +28 -5
- package/src/ui-tailwind/chart/render/pie.tsx +36 -16
- package/src/ui-tailwind/chart/render/progressive-render.ts +8 -1
- package/src/ui-tailwind/chart/render/scatter.tsx +9 -4
- package/src/ui-tailwind/chrome/avatar-initials.ts +15 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +3 -1
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +14 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +3 -2
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +30 -11
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +15 -2
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +1 -1
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +24 -7
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +31 -12
- package/src/ui-tailwind/chrome-overlay/page-border-resolver.ts +211 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +24 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +74 -0
- package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +65 -0
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +157 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-border-overlay.tsx +233 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +135 -13
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +51 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +12 -4
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +32 -12
- package/src/ui-tailwind/chrome-overlay/tw-toc-outline-sidebar.tsx +133 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +49 -10
- package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +119 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +236 -9
- package/src/ui-tailwind/editor-surface/pm-schema.ts +214 -11
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +32 -2
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +206 -0
- package/src/ui-tailwind/editor-surface/surface-layer.ts +66 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +29 -0
- package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +22 -6
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +10 -16
- package/src/ui-tailwind/review/tw-health-panel.tsx +0 -25
- package/src/ui-tailwind/review/tw-rail-card.tsx +38 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -2
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +5 -12
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +2 -2
- package/src/ui-tailwind/theme/editor-theme.css +1 -0
- package/src/ui-tailwind/theme/tokens.css +6 -0
- package/src/ui-tailwind/theme/tokens.ts +10 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +23 -0
- package/src/validation/compatibility-engine.ts +2 -0
- 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
|
-
//
|
|
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 {
|