@beyondwork/docx-react-component 1.0.53 → 1.0.55
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/package.json +1 -1
- package/src/api/public-types.ts +125 -7
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +27 -3
- package/src/io/normalize/normalize-text.ts +1 -0
- package/src/io/ooxml/parse-field-switches.ts +134 -0
- package/src/io/ooxml/parse-fields.ts +28 -2
- package/src/model/canonical-document.ts +13 -2
- package/src/runtime/chart/chart-model-store.ts +88 -0
- package/src/runtime/chart/chart-snapshot.ts +239 -0
- package/src/runtime/collab/checkpoint-store.ts +1 -1
- package/src/runtime/collab/event-types.ts +4 -0
- package/src/runtime/collab/runtime-collab-sync.ts +1 -2
- package/src/runtime/document-runtime.ts +115 -13
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +58 -1
- package/src/runtime/layout/layout-invalidation.ts +150 -30
- package/src/runtime/layout/page-graph.ts +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +128 -19
- package/src/runtime/layout/project-block-fragments.ts +27 -0
- package/src/runtime/layout/public-facet.ts +27 -0
- package/src/runtime/page-number-format.ts +207 -0
- package/src/runtime/render/render-frame-diff.ts +38 -2
- package/src/runtime/surface-projection.ts +32 -3
- package/src/ui/WordReviewEditor.tsx +57 -3
- package/src/ui/headless/comment-decoration-model.ts +60 -5
- package/src/ui/headless/revision-decoration-model.ts +94 -6
- package/src/ui/shared/revision-filters.ts +16 -6
- package/src/ui-tailwind/chart/ChartSurface.tsx +236 -0
- package/src/ui-tailwind/chart/layout/axis-layout.ts +17 -9
- package/src/ui-tailwind/chart/layout/legend-layout.ts +231 -0
- package/src/ui-tailwind/chart/layout/plot-area.ts +152 -59
- package/src/ui-tailwind/chart/layout/title-layout.ts +184 -0
- package/src/ui-tailwind/chart/render/area.tsx +277 -0
- package/src/ui-tailwind/chart/render/bar-column.tsx +356 -0
- package/src/ui-tailwind/chart/render/bubble.tsx +134 -0
- package/src/ui-tailwind/chart/render/combo.tsx +85 -0
- package/src/ui-tailwind/chart/render/data-labels.tsx +513 -0
- package/src/ui-tailwind/chart/render/font-metrics.ts +298 -0
- package/src/ui-tailwind/chart/render/gridlines.ts +228 -0
- package/src/ui-tailwind/chart/render/line.tsx +363 -0
- package/src/ui-tailwind/chart/render/number-format.ts +120 -16
- package/src/ui-tailwind/chart/render/pie.tsx +275 -0
- package/src/ui-tailwind/chart/render/progressive-render.ts +103 -0
- package/src/ui-tailwind/chart/render/scatter.tsx +228 -0
- package/src/ui-tailwind/chart/render/smooth-curve.ts +101 -0
- package/src/ui-tailwind/chart/render/svg-primitives.ts +378 -0
- package/src/ui-tailwind/chart/render/unsupported.tsx +126 -0
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +11 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +44 -18
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +68 -7
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +21 -2
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +20 -3
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +102 -37
- package/src/ui-tailwind/chrome/tw-command-palette.tsx +358 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +108 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +227 -0
- package/src/ui-tailwind/chrome/tw-display-mode-selector.tsx +136 -0
- package/src/ui-tailwind/chrome/tw-empty-state.tsx +76 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +30 -16
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +23 -4
- package/src/ui-tailwind/chrome/tw-paste-drop-toast.tsx +113 -0
- package/src/ui-tailwind/chrome/tw-revision-hover-preview.tsx +150 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-formatting.tsx +2 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +38 -2
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +15 -3
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +32 -20
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +68 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +10 -10
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +26 -5
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +29 -22
- package/src/ui-tailwind/chrome/tw-unsaved-modal.tsx +72 -10
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +33 -18
- package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +94 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +90 -0
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +20 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +4 -0
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +14 -0
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +93 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +2 -1
- package/src/ui-tailwind/index.ts +11 -0
- package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +52 -2
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +13 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +13 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +8 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +83 -32
- package/src/ui-tailwind/review/tw-health-panel.tsx +174 -109
- package/src/ui-tailwind/review/tw-rail-card.tsx +9 -1
- package/src/ui-tailwind/review/tw-review-rail.tsx +36 -42
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +189 -101
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +11 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +114 -46
- package/src/ui-tailwind/theme/editor-theme.css +249 -22
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +14 -1
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +73 -32
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +49 -9
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +178 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +39 -6
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +0 -85
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beyondwork/docx-react-component",
|
|
3
3
|
"publisher": "beyondwork",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.55",
|
|
5
5
|
"description": "Embeddable React Word (docx) editor with review, comments, tracked changes, and round-trip OOXML fidelity.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": [
|
package/src/api/public-types.ts
CHANGED
|
@@ -858,6 +858,13 @@ export type SurfaceInlineSegment =
|
|
|
858
858
|
* a text badge. Absent when the object has no cached fallback image.
|
|
859
859
|
*/
|
|
860
860
|
previewMediaId?: string;
|
|
861
|
+
/**
|
|
862
|
+
* Stable chart ID (Stage 6). Present when the `chart_preview` node has a
|
|
863
|
+
* successfully parsed `ChartModel` (`parsedData` populated at import time).
|
|
864
|
+
* The ID keys into the module-level `chartModelStore` so `ChartNodeView`
|
|
865
|
+
* can render `ChartSurface` instead of the fallback bitmap or badge.
|
|
866
|
+
*/
|
|
867
|
+
parsedChartId?: string;
|
|
861
868
|
state: "locked-preserve-only";
|
|
862
869
|
}
|
|
863
870
|
| {
|
|
@@ -2846,6 +2853,80 @@ export interface EditorTelemetryEvent {
|
|
|
2846
2853
|
detail?: Record<string, unknown>;
|
|
2847
2854
|
}
|
|
2848
2855
|
|
|
2856
|
+
// ---------------------------------------------------------------------------
|
|
2857
|
+
// Chart read-model (Stage 6)
|
|
2858
|
+
// ---------------------------------------------------------------------------
|
|
2859
|
+
|
|
2860
|
+
/**
|
|
2861
|
+
* Structured, typed representation of a chart's data. Returned by
|
|
2862
|
+
* `WordReviewEditorRef.getChartSnapshot` so agents can reason over chart
|
|
2863
|
+
* content without re-parsing `rawXml`. Charts are read-only; agents must not
|
|
2864
|
+
* mutate this object.
|
|
2865
|
+
*/
|
|
2866
|
+
export interface ChartSnapshot {
|
|
2867
|
+
chartId: string;
|
|
2868
|
+
kind: string;
|
|
2869
|
+
title?: string;
|
|
2870
|
+
seriesCount: number;
|
|
2871
|
+
categoryCount: number;
|
|
2872
|
+
data: ChartSnapshotData;
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
export type ChartSnapshotData =
|
|
2876
|
+
| {
|
|
2877
|
+
kind: "bar";
|
|
2878
|
+
direction: "bar" | "column";
|
|
2879
|
+
grouping: "clustered" | "stacked" | "percentStacked" | "standard";
|
|
2880
|
+
series: ChartSnapshotSeries[];
|
|
2881
|
+
categories: string[];
|
|
2882
|
+
}
|
|
2883
|
+
| {
|
|
2884
|
+
kind: "line";
|
|
2885
|
+
grouping: "standard" | "stacked" | "percentStacked";
|
|
2886
|
+
series: ChartSnapshotSeries[];
|
|
2887
|
+
categories: string[];
|
|
2888
|
+
}
|
|
2889
|
+
| {
|
|
2890
|
+
kind: "pie";
|
|
2891
|
+
doughnut: boolean;
|
|
2892
|
+
series: ChartSnapshotSeries[];
|
|
2893
|
+
categories: string[];
|
|
2894
|
+
}
|
|
2895
|
+
| {
|
|
2896
|
+
kind: "area";
|
|
2897
|
+
grouping: "standard" | "stacked" | "percentStacked";
|
|
2898
|
+
series: ChartSnapshotSeries[];
|
|
2899
|
+
categories: string[];
|
|
2900
|
+
}
|
|
2901
|
+
| { kind: "scatter"; series: ChartSnapshotScatterSeries[] }
|
|
2902
|
+
| { kind: "bubble"; series: ChartSnapshotBubbleSeries[] }
|
|
2903
|
+
| {
|
|
2904
|
+
kind: "combo";
|
|
2905
|
+
groups: Array<{ kind: "bar" | "line" | "area"; series: ChartSnapshotSeries[] }>;
|
|
2906
|
+
categories: string[];
|
|
2907
|
+
}
|
|
2908
|
+
| { kind: "unsupported"; reason: string };
|
|
2909
|
+
|
|
2910
|
+
export interface ChartSnapshotSeries {
|
|
2911
|
+
name?: string;
|
|
2912
|
+
values: Array<number | null>;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
export interface ChartSnapshotScatterSeries {
|
|
2916
|
+
name?: string;
|
|
2917
|
+
xValues: Array<number | null>;
|
|
2918
|
+
yValues: Array<number | null>;
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
export interface ChartSnapshotBubbleSeries {
|
|
2922
|
+
name?: string;
|
|
2923
|
+
xValues: Array<number | null>;
|
|
2924
|
+
yValues: Array<number | null>;
|
|
2925
|
+
sizes: Array<number | null>;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
// ---------------------------------------------------------------------------
|
|
2929
|
+
|
|
2849
2930
|
/**
|
|
2850
2931
|
* Stage 0B.1 — parameters passed to `EditorHostAdapter.renderChartPreview`
|
|
2851
2932
|
* when the importer encounters a `c:chartSpace` that has no cached
|
|
@@ -2949,6 +3030,15 @@ export interface WordReviewEditorRef {
|
|
|
2949
3030
|
getRenderSnapshot(): RuntimeRenderSnapshot;
|
|
2950
3031
|
getCompatibilityReport(): CompatibilityReport;
|
|
2951
3032
|
getWarnings(): EditorWarning[];
|
|
3033
|
+
/**
|
|
3034
|
+
* Stage 6 — structured chart data for a specific chart by ID. Returns `null`
|
|
3035
|
+
* when the chart ID is not found or the chart has no parsed model (e.g. parse
|
|
3036
|
+
* failed at import time, or chart type is unsupported). Use `getChartSnapshots`
|
|
3037
|
+
* to enumerate all charts in the document.
|
|
3038
|
+
*/
|
|
3039
|
+
getChartSnapshot(chartId: string): ChartSnapshot | null;
|
|
3040
|
+
/** Stage 6 — all charts in the document that have a parsed model. */
|
|
3041
|
+
getChartSnapshots(): ChartSnapshot[];
|
|
2952
3042
|
getCommentSidebarSnapshot(): CommentSidebarSnapshot;
|
|
2953
3043
|
getTrackedChangesSnapshot(): TrackedChangesSnapshot;
|
|
2954
3044
|
getSuggestionsSnapshot(): SuggestionsSnapshot;
|
|
@@ -3058,12 +3148,16 @@ export interface WordReviewEditorRef {
|
|
|
3058
3148
|
setWorkflowOverlay(overlay: WorkflowOverlay): void;
|
|
3059
3149
|
clearWorkflowOverlay(): void;
|
|
3060
3150
|
/**
|
|
3061
|
-
*
|
|
3062
|
-
* overlay
|
|
3063
|
-
* `
|
|
3064
|
-
*
|
|
3065
|
-
*
|
|
3066
|
-
*
|
|
3151
|
+
* Return a structural clone of the currently-registered workflow
|
|
3152
|
+
* overlay, or `null` when no overlay has been set (or
|
|
3153
|
+
* `clearWorkflowOverlay` has been called). Intended for the canonical
|
|
3154
|
+
* host read-before-write pattern — read current, merge host-owned
|
|
3155
|
+
* fields (e.g. `candidates`), write back via `setWorkflowOverlay`
|
|
3156
|
+
* without clobbering engine-authored `scopes`, `workItems`, or the
|
|
3157
|
+
* overlay-level `metadataPersistence` default. `setWorkflowOverlay`
|
|
3158
|
+
* replaces the overlay wholesale; passing `scopes: []` will drop
|
|
3159
|
+
* every scope registered via `addScope` and cause subsequent
|
|
3160
|
+
* `replaceText` calls to block with `workflow_comment_only`.
|
|
3067
3161
|
*/
|
|
3068
3162
|
getWorkflowOverlay(): WorkflowOverlay | null;
|
|
3069
3163
|
setSharedWorkflowState(state: SharedWorkflowState | null): void;
|
|
@@ -3304,7 +3398,31 @@ export interface WordReviewEditorProps {
|
|
|
3304
3398
|
suggestionsEnabled?: boolean;
|
|
3305
3399
|
chromePreset?: WordReviewEditorChromePreset;
|
|
3306
3400
|
chromeOptions?: Partial<WordReviewEditorChromeOptions>;
|
|
3307
|
-
|
|
3401
|
+
/**
|
|
3402
|
+
* Controls how tracked changes and comments render. Accepts Word's
|
|
3403
|
+
* 4-mode grammar (`"all-markup" | "simple-markup" | "no-markup" | "original"`)
|
|
3404
|
+
* or the legacy triple (`"all" | "simple" | "clean"`), which maps 1:1
|
|
3405
|
+
* onto the first three canonical names. `"original"` is new in 6d.N2:
|
|
3406
|
+
* insertions are hidden, deletions render as plain body text so the
|
|
3407
|
+
* reviewer sees the pre-change state.
|
|
3408
|
+
*/
|
|
3409
|
+
markupDisplay?:
|
|
3410
|
+
| "all-markup"
|
|
3411
|
+
| "simple-markup"
|
|
3412
|
+
| "no-markup"
|
|
3413
|
+
| "original"
|
|
3414
|
+
| "clean"
|
|
3415
|
+
| "simple"
|
|
3416
|
+
| "all";
|
|
3417
|
+
/**
|
|
3418
|
+
* L6d.N2 — invoked when the user picks a different display mode from
|
|
3419
|
+
* the in-chrome selector. Values are always emitted in the canonical
|
|
3420
|
+
* Word grammar (`"all-markup" | "simple-markup" | "no-markup" | "original"`),
|
|
3421
|
+
* not the legacy aliases.
|
|
3422
|
+
*/
|
|
3423
|
+
onMarkupDisplayChange?: (
|
|
3424
|
+
value: "all-markup" | "simple-markup" | "no-markup" | "original",
|
|
3425
|
+
) => void;
|
|
3308
3426
|
/**
|
|
3309
3427
|
* @internal HARNESS-ONLY debug-ports token.
|
|
3310
3428
|
*
|
package/src/index.ts
CHANGED
|
@@ -122,6 +122,11 @@ export type {
|
|
|
122
122
|
EditorSessionState,
|
|
123
123
|
EditorHostAdapter,
|
|
124
124
|
ChartPreviewResolveParams,
|
|
125
|
+
ChartSnapshot,
|
|
126
|
+
ChartSnapshotData,
|
|
127
|
+
ChartSnapshotSeries,
|
|
128
|
+
ChartSnapshotScatterSeries,
|
|
129
|
+
ChartSnapshotBubbleSeries,
|
|
125
130
|
WordReviewEditorProps,
|
|
126
131
|
WordReviewEditorChromePreset,
|
|
127
132
|
WordReviewEditorChromeOptions,
|
package/src/io/docx-session.ts
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
getDocumentBackedWorkflowMetadata,
|
|
68
68
|
parseWorkflowPayloadEnvelopeFromPackage,
|
|
69
69
|
resolvePayloadPartPath,
|
|
70
|
+
resolveWorkflowPayloadPartPaths,
|
|
70
71
|
WORKFLOW_PAYLOAD_CONTENT_TYPE,
|
|
71
72
|
WORKFLOW_PAYLOAD_CUSTOM_PROPS_CONTENT_TYPE,
|
|
72
73
|
WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
|
|
@@ -2105,13 +2106,17 @@ function exportDocxEditorSession(
|
|
|
2105
2106
|
const hasSettingsSurface =
|
|
2106
2107
|
Boolean(state.sourceSettingsPartPath) ||
|
|
2107
2108
|
exportedSubParts?.settings !== undefined;
|
|
2109
|
+
const workflowPayloadPartPaths = resolveWorkflowPayloadPartPaths(
|
|
2110
|
+
state.sourcePackage,
|
|
2111
|
+
sessionState.documentId,
|
|
2112
|
+
);
|
|
2108
2113
|
|
|
2109
2114
|
const exportSession = createExportSession(state.sourcePackage, [
|
|
2110
2115
|
state.sourceDocumentPartPath,
|
|
2111
2116
|
APP_PROPERTIES_PART_PATH,
|
|
2112
2117
|
CORE_PROPERTIES_PART_PATH,
|
|
2113
|
-
|
|
2114
|
-
|
|
2118
|
+
workflowPayloadPartPaths.payloadPartPath,
|
|
2119
|
+
workflowPayloadPartPaths.itemPropsPartPath,
|
|
2115
2120
|
WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
|
|
2116
2121
|
numberingPartPath,
|
|
2117
2122
|
commentsPartPath,
|
|
@@ -2361,7 +2366,14 @@ function exportDocxEditorSession(
|
|
|
2361
2366
|
ensureHostMetadataParts(exportSession, state.sourcePackage, currentDocument);
|
|
2362
2367
|
// Schema 1.2: pass through editorState payload collected by the runtime channel.
|
|
2363
2368
|
const internalEditorState = (options as { _editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload } | undefined)?._editorState;
|
|
2364
|
-
ensureWorkflowPayloadParts(
|
|
2369
|
+
ensureWorkflowPayloadParts(
|
|
2370
|
+
exportSession,
|
|
2371
|
+
sessionState,
|
|
2372
|
+
currentDocument,
|
|
2373
|
+
state.sourcePackage,
|
|
2374
|
+
workflowPayloadPartPaths,
|
|
2375
|
+
internalEditorState,
|
|
2376
|
+
);
|
|
2365
2377
|
|
|
2366
2378
|
return {
|
|
2367
2379
|
bytes: exportSession.serialize(),
|
|
@@ -4087,6 +4099,10 @@ function ensureWorkflowPayloadParts(
|
|
|
4087
4099
|
sessionState: EditorSessionState,
|
|
4088
4100
|
document: CanonicalDocumentEnvelope,
|
|
4089
4101
|
sourcePackage: OpcPackage,
|
|
4102
|
+
resolvedPartPaths: {
|
|
4103
|
+
payloadPartPath: string;
|
|
4104
|
+
itemPropsPartPath: string;
|
|
4105
|
+
},
|
|
4090
4106
|
editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload,
|
|
4091
4107
|
): void {
|
|
4092
4108
|
const payloadParts = buildWorkflowPayloadParts({
|
|
@@ -4102,6 +4118,14 @@ function ensureWorkflowPayloadParts(
|
|
|
4102
4118
|
if (!payloadParts) {
|
|
4103
4119
|
return;
|
|
4104
4120
|
}
|
|
4121
|
+
if (
|
|
4122
|
+
payloadParts.payloadPartPath !== resolvedPartPaths.payloadPartPath ||
|
|
4123
|
+
payloadParts.itemPropsPartPath !== resolvedPartPaths.itemPropsPartPath
|
|
4124
|
+
) {
|
|
4125
|
+
throw new Error(
|
|
4126
|
+
"Workflow payload export resolved inconsistent customXml paths; export session ownership no longer matches payload serialization.",
|
|
4127
|
+
);
|
|
4128
|
+
}
|
|
4105
4129
|
|
|
4106
4130
|
const payloadPart = sourcePackage.parts.get(payloadParts.payloadPartPath);
|
|
4107
4131
|
const itemPropsPart = sourcePackage.parts.get(payloadParts.itemPropsPartPath);
|
|
@@ -560,6 +560,7 @@ function normalizeInlineChildren(
|
|
|
560
560
|
children: fieldChildren,
|
|
561
561
|
fieldFamily: classification.family,
|
|
562
562
|
...(classification.target ? { fieldTarget: classification.target } : {}),
|
|
563
|
+
...(classification.switches ? { switches: classification.switches } : {}),
|
|
563
564
|
refreshStatus: classification.supported ? "stale" : "preserve-only",
|
|
564
565
|
});
|
|
565
566
|
state.cursor += renderedLength > 0 ? renderedLength : 1;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* parse-field-switches.ts
|
|
3
|
+
*
|
|
4
|
+
* Pure tokenizer for OOXML field instruction switches.
|
|
5
|
+
* Used by parse-fields.ts to populate FieldNode.switches for REF/PAGEREF/NOTEREF/TOC.
|
|
6
|
+
*
|
|
7
|
+
* No runtime dependencies — this lives in the io layer alongside parse-fields.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface ParsedFieldSwitches {
|
|
11
|
+
/** \h — render as hyperlink */
|
|
12
|
+
hyperlink: boolean;
|
|
13
|
+
/** \p — emit relative position (above/below/on this page) */
|
|
14
|
+
relativePosition: boolean;
|
|
15
|
+
/** \n — use numeric reference */
|
|
16
|
+
numericRef: boolean;
|
|
17
|
+
/** \r — recursive */
|
|
18
|
+
recursive: boolean;
|
|
19
|
+
/** \t — suppress non-delimiter characters */
|
|
20
|
+
suppressNonDelimiter: boolean;
|
|
21
|
+
/** \w — include numbering prefix */
|
|
22
|
+
includeNumbering: boolean;
|
|
23
|
+
/** \l — include level */
|
|
24
|
+
includeLevel: boolean;
|
|
25
|
+
/** Any unrecognized switch letters (e.g. \*, \@, \#) */
|
|
26
|
+
unknownSwitches: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tokenize a field instruction string.
|
|
31
|
+
* Quoted strings (starting with ") are treated as a single token.
|
|
32
|
+
* Other tokens are split on whitespace.
|
|
33
|
+
*/
|
|
34
|
+
function tokenizeInstruction(s: string): string[] {
|
|
35
|
+
const tokens: string[] = [];
|
|
36
|
+
let i = 0;
|
|
37
|
+
while (i < s.length) {
|
|
38
|
+
// Skip whitespace
|
|
39
|
+
while (i < s.length && /\s/.test(s[i]!)) i += 1;
|
|
40
|
+
if (i >= s.length) break;
|
|
41
|
+
|
|
42
|
+
if (s[i] === '"') {
|
|
43
|
+
// Quoted token — scan to closing quote
|
|
44
|
+
const start = i;
|
|
45
|
+
i += 1; // skip opening quote
|
|
46
|
+
while (i < s.length && s[i] !== '"') i += 1;
|
|
47
|
+
if (i < s.length) i += 1; // skip closing quote
|
|
48
|
+
tokens.push(s.slice(start, i));
|
|
49
|
+
} else {
|
|
50
|
+
// Normal whitespace-delimited token
|
|
51
|
+
const start = i;
|
|
52
|
+
while (i < s.length && !/\s/.test(s[i]!)) i += 1;
|
|
53
|
+
tokens.push(s.slice(start, i));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return tokens;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse field switches from a raw field instruction string.
|
|
61
|
+
* Handles case-insensitive switch letters (\h, \p, \n, \r, \t, \w, \l).
|
|
62
|
+
* General-purpose switches (\*, \@, \#) each consume the next token as argument.
|
|
63
|
+
*/
|
|
64
|
+
export function parseFieldSwitches(instruction: string): ParsedFieldSwitches {
|
|
65
|
+
const result: ParsedFieldSwitches = {
|
|
66
|
+
hyperlink: false,
|
|
67
|
+
relativePosition: false,
|
|
68
|
+
numericRef: false,
|
|
69
|
+
recursive: false,
|
|
70
|
+
suppressNonDelimiter: false,
|
|
71
|
+
includeNumbering: false,
|
|
72
|
+
includeLevel: false,
|
|
73
|
+
unknownSwitches: [],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const tokens = tokenizeInstruction(instruction);
|
|
77
|
+
let i = 0;
|
|
78
|
+
|
|
79
|
+
// Skip leading non-switch tokens (field name, target bookmark, quoted names)
|
|
80
|
+
while (i < tokens.length && !tokens[i]!.startsWith("\\")) {
|
|
81
|
+
i += 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Process switch tokens
|
|
85
|
+
while (i < tokens.length) {
|
|
86
|
+
const token = tokens[i]!;
|
|
87
|
+
i += 1;
|
|
88
|
+
|
|
89
|
+
if (!token.startsWith("\\")) {
|
|
90
|
+
// Not a switch — skip (e.g. argument to a previous switch)
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const lower = token.toLowerCase();
|
|
95
|
+
|
|
96
|
+
switch (lower) {
|
|
97
|
+
case "\\h":
|
|
98
|
+
result.hyperlink = true;
|
|
99
|
+
break;
|
|
100
|
+
case "\\p":
|
|
101
|
+
result.relativePosition = true;
|
|
102
|
+
break;
|
|
103
|
+
case "\\n":
|
|
104
|
+
result.numericRef = true;
|
|
105
|
+
break;
|
|
106
|
+
case "\\r":
|
|
107
|
+
result.recursive = true;
|
|
108
|
+
break;
|
|
109
|
+
case "\\t":
|
|
110
|
+
result.suppressNonDelimiter = true;
|
|
111
|
+
break;
|
|
112
|
+
case "\\w":
|
|
113
|
+
result.includeNumbering = true;
|
|
114
|
+
break;
|
|
115
|
+
case "\\l":
|
|
116
|
+
result.includeLevel = true;
|
|
117
|
+
break;
|
|
118
|
+
case "\\*":
|
|
119
|
+
case "\\@":
|
|
120
|
+
case "\\#":
|
|
121
|
+
// General-purpose switches — consume next token as argument
|
|
122
|
+
result.unknownSwitches.push(token);
|
|
123
|
+
if (i < tokens.length) {
|
|
124
|
+
i += 1; // consume argument
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
result.unknownSwitches.push(token);
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
@@ -211,9 +211,10 @@ import type {
|
|
|
211
211
|
TocEntry,
|
|
212
212
|
TocStructure,
|
|
213
213
|
} from "../../model/canonical-document.ts";
|
|
214
|
+
import { parseFieldSwitches } from "./parse-field-switches.ts";
|
|
214
215
|
|
|
215
216
|
const FIELD_FAMILY_PATTERN =
|
|
216
|
-
/^\s*(REF|PAGEREF|NOTEREF|TOC|PAGE|NUMPAGES|DATE|TIME|AUTHOR|FILENAME|MERGEFIELD|IF|SEQ|INDEX|TC|STYLEREF)\b/i;
|
|
217
|
+
/^\s*(REF|PAGEREF|NOTEREF|TOC|PAGE|NUMPAGES|DATE|TIME|AUTHOR|FILENAME|MERGEFIELD|IF|SEQ|INDEX|TC|STYLEREF|SECTIONPAGES)\b/i;
|
|
217
218
|
|
|
218
219
|
const SUPPORTED_FAMILIES = new Set<string>([
|
|
219
220
|
"REF",
|
|
@@ -222,16 +223,20 @@ const SUPPORTED_FAMILIES = new Set<string>([
|
|
|
222
223
|
"TOC",
|
|
223
224
|
"PAGE",
|
|
224
225
|
"NUMPAGES",
|
|
226
|
+
"STYLEREF",
|
|
227
|
+
"SECTIONPAGES",
|
|
225
228
|
]);
|
|
226
229
|
|
|
227
230
|
/**
|
|
228
231
|
* Classify a field instruction into its field family.
|
|
229
232
|
* Returns the family enum value and whether it is in the supported slice.
|
|
233
|
+
* For REF/PAGEREF/NOTEREF/TOC families, also parses field switches.
|
|
230
234
|
*/
|
|
231
235
|
export function classifyFieldInstruction(instruction: string): {
|
|
232
236
|
family: FieldFamily;
|
|
233
237
|
supported: boolean;
|
|
234
238
|
target?: string;
|
|
239
|
+
switches?: FieldNode["switches"];
|
|
235
240
|
} {
|
|
236
241
|
const trimmed = instruction.trim();
|
|
237
242
|
const match = FIELD_FAMILY_PATTERN.exec(trimmed);
|
|
@@ -247,8 +252,29 @@ export function classifyFieldInstruction(instruction: string): {
|
|
|
247
252
|
const targetMatch = /^\s*(?:REF|PAGEREF|NOTEREF)\s+(?:"([^"]+)"|(\S+))/i.exec(trimmed);
|
|
248
253
|
target = (targetMatch?.[1] ?? targetMatch?.[2])?.trim();
|
|
249
254
|
}
|
|
255
|
+
if (family === "STYLEREF") {
|
|
256
|
+
const styleMatch = /^\s*STYLEREF\s+(?:"([^"]+)"|(\S+))/i.exec(trimmed);
|
|
257
|
+
const extracted = styleMatch?.[1] ?? styleMatch?.[2];
|
|
258
|
+
if (extracted) target = extracted.trim();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let switches: FieldNode["switches"] | undefined;
|
|
262
|
+
if (family === "REF" || family === "PAGEREF" || family === "NOTEREF" || family === "TOC") {
|
|
263
|
+
const raw = parseFieldSwitches(trimmed);
|
|
264
|
+
const sw: FieldNode["switches"] = {};
|
|
265
|
+
if (raw.hyperlink) sw.hyperlink = true;
|
|
266
|
+
if (raw.relativePosition) sw.relativePosition = true;
|
|
267
|
+
if (raw.numericRef) sw.numericRef = true;
|
|
268
|
+
if (raw.recursive) sw.recursive = true;
|
|
269
|
+
if (raw.suppressNonDelimiter) sw.suppressNonDelimiter = true;
|
|
270
|
+
if (raw.includeNumbering) sw.includeNumbering = true;
|
|
271
|
+
if (raw.includeLevel) sw.includeLevel = true;
|
|
272
|
+
if (Object.keys(sw).length > 0) {
|
|
273
|
+
switches = sw;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
250
276
|
|
|
251
|
-
return { family, supported, target };
|
|
277
|
+
return { family, supported, target, ...(switches ? { switches } : {}) };
|
|
252
278
|
}
|
|
253
279
|
|
|
254
280
|
/**
|
|
@@ -804,7 +804,9 @@ export type SupportedFieldFamily =
|
|
|
804
804
|
| "NOTEREF"
|
|
805
805
|
| "TOC"
|
|
806
806
|
| "PAGE"
|
|
807
|
-
| "NUMPAGES"
|
|
807
|
+
| "NUMPAGES"
|
|
808
|
+
| "STYLEREF"
|
|
809
|
+
| "SECTIONPAGES";
|
|
808
810
|
|
|
809
811
|
/**
|
|
810
812
|
* Unsupported field families that remain preserve-only.
|
|
@@ -820,7 +822,6 @@ export type PreserveOnlyFieldFamily =
|
|
|
820
822
|
| "SEQ"
|
|
821
823
|
| "INDEX"
|
|
822
824
|
| "TC"
|
|
823
|
-
| "STYLEREF"
|
|
824
825
|
| "FORMULA"
|
|
825
826
|
| "UNKNOWN";
|
|
826
827
|
|
|
@@ -844,6 +845,16 @@ export interface FieldNode {
|
|
|
844
845
|
fieldTarget?: string;
|
|
845
846
|
/** Runtime refresh status. Undefined for legacy or preserve-only fields. */
|
|
846
847
|
refreshStatus?: FieldRefreshStatus;
|
|
848
|
+
/** Parsed field switches. Present only when the instruction contains recognized switches. */
|
|
849
|
+
switches?: {
|
|
850
|
+
hyperlink?: boolean;
|
|
851
|
+
relativePosition?: boolean;
|
|
852
|
+
numericRef?: boolean;
|
|
853
|
+
recursive?: boolean;
|
|
854
|
+
suppressNonDelimiter?: boolean;
|
|
855
|
+
includeNumbering?: boolean;
|
|
856
|
+
includeLevel?: boolean;
|
|
857
|
+
};
|
|
847
858
|
}
|
|
848
859
|
|
|
849
860
|
// ─── Field registry ─────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level cache of parsed ChartModel instances keyed by a stable chart ID.
|
|
3
|
+
* Populated as a side effect of `surface-projection.ts` when a `chart_preview`
|
|
4
|
+
* node carries `parsedData`. Consumed by `ChartNodeView` to render `ChartSurface`.
|
|
5
|
+
*
|
|
6
|
+
* Charts are preserve-only (rawXml never mutated), so entries are stable for the
|
|
7
|
+
* lifetime of the document. `clear()` is called before repopulating on document
|
|
8
|
+
* replacement.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ChartModel } from "../../io/ooxml/chart/types.ts";
|
|
12
|
+
import type { ResolvedTheme } from "../../model/canonical-document.ts";
|
|
13
|
+
|
|
14
|
+
export interface ChartStoreEntry {
|
|
15
|
+
readonly model: ChartModel;
|
|
16
|
+
readonly widthPx: number;
|
|
17
|
+
readonly heightPx: number;
|
|
18
|
+
readonly theme: ResolvedTheme | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const _store = new Map<string, ChartStoreEntry>();
|
|
22
|
+
|
|
23
|
+
export const chartModelStore = {
|
|
24
|
+
set(chartId: string, entry: ChartStoreEntry): void {
|
|
25
|
+
_store.set(chartId, entry);
|
|
26
|
+
},
|
|
27
|
+
get(chartId: string): ChartStoreEntry | undefined {
|
|
28
|
+
return _store.get(chartId);
|
|
29
|
+
},
|
|
30
|
+
clear(): void {
|
|
31
|
+
_store.clear();
|
|
32
|
+
},
|
|
33
|
+
size(): number {
|
|
34
|
+
return _store.size;
|
|
35
|
+
},
|
|
36
|
+
has(chartId: string): boolean {
|
|
37
|
+
return _store.has(chartId);
|
|
38
|
+
},
|
|
39
|
+
values(): IterableIterator<ChartStoreEntry> {
|
|
40
|
+
return _store.values();
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const EMU_PER_PX = 9525;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract drawing extent from rawXml and convert EMU → pixels.
|
|
48
|
+
* Tries `wp:extent` first (wp:inline/wp:anchor drawings), then `a:ext` as
|
|
49
|
+
* fallback (xfrm transforms). Falls back to a sensible 6×3.5 inch default.
|
|
50
|
+
*/
|
|
51
|
+
export function extractChartDimensions(rawXml: string): { widthPx: number; heightPx: number } {
|
|
52
|
+
const extentMatch = /<wp:extent\b([^/>]*)\/?>/.exec(rawXml);
|
|
53
|
+
if (extentMatch) {
|
|
54
|
+
const attrs = extentMatch[1] ?? "";
|
|
55
|
+
const cx = /\bcx="(\d+)"/.exec(attrs)?.[1];
|
|
56
|
+
const cy = /\bcy="(\d+)"/.exec(attrs)?.[1];
|
|
57
|
+
if (cx && cy) {
|
|
58
|
+
return {
|
|
59
|
+
widthPx: Math.round(parseInt(cx, 10) / EMU_PER_PX),
|
|
60
|
+
heightPx: Math.round(parseInt(cy, 10) / EMU_PER_PX),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const aExtMatch = /\ba:ext\s[^>]*\bcx="(\d+)"[^>]*\bcy="(\d+)"/.exec(rawXml);
|
|
65
|
+
if (aExtMatch) {
|
|
66
|
+
return {
|
|
67
|
+
widthPx: Math.round(parseInt(aExtMatch[1]!, 10) / EMU_PER_PX),
|
|
68
|
+
heightPx: Math.round(parseInt(aExtMatch[2]!, 10) / EMU_PER_PX),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { widthPx: 576, heightPx: 336 };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Stable chart ID derived from rawXml content. Charts are never mutated so the
|
|
76
|
+
* ID is stable for the lifetime of the document load.
|
|
77
|
+
*
|
|
78
|
+
* Uses djb2 hash + length suffix to minimize collisions without importing a
|
|
79
|
+
* crypto dependency.
|
|
80
|
+
*/
|
|
81
|
+
export function stableChartId(rawXml: string): string {
|
|
82
|
+
let h = 5381;
|
|
83
|
+
for (let i = 0; i < rawXml.length; i++) {
|
|
84
|
+
h = ((h << 5) + h) ^ rawXml.charCodeAt(i);
|
|
85
|
+
h = h | 0;
|
|
86
|
+
}
|
|
87
|
+
return `chart-${(h >>> 0).toString(16)}-${rawXml.length}`;
|
|
88
|
+
}
|