@beyondwork/docx-react-component 1.0.54 → 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.
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Public read-model for chart data. Projected from a `ChartModel` parsed during
3
+ * DOCX import. Agents consume this without re-parsing `rawXml`.
4
+ */
5
+
6
+ import type {
7
+ ChartModel,
8
+ BarChartModel,
9
+ LineChartModel,
10
+ PieChartModel,
11
+ AreaChartModel,
12
+ ScatterChartModel,
13
+ BubbleChartModel,
14
+ ComboChartModel,
15
+ } from "../../io/ooxml/chart/types.ts";
16
+
17
+ export interface ChartSnapshot {
18
+ chartId: string;
19
+ kind: ChartModel["kind"];
20
+ title?: string;
21
+ seriesCount: number;
22
+ categoryCount: number;
23
+ data: ChartSnapshotData;
24
+ }
25
+
26
+ export type ChartSnapshotData =
27
+ | {
28
+ kind: "bar";
29
+ direction: "bar" | "column";
30
+ grouping: "clustered" | "stacked" | "percentStacked" | "standard";
31
+ series: ChartSnapshotSeries[];
32
+ categories: string[];
33
+ }
34
+ | {
35
+ kind: "line";
36
+ grouping: "standard" | "stacked" | "percentStacked";
37
+ series: ChartSnapshotSeries[];
38
+ categories: string[];
39
+ }
40
+ | {
41
+ kind: "pie";
42
+ doughnut: boolean;
43
+ series: ChartSnapshotSeries[];
44
+ categories: string[];
45
+ }
46
+ | {
47
+ kind: "area";
48
+ grouping: "standard" | "stacked" | "percentStacked";
49
+ series: ChartSnapshotSeries[];
50
+ categories: string[];
51
+ }
52
+ | { kind: "scatter"; series: ChartSnapshotScatterSeries[] }
53
+ | { kind: "bubble"; series: ChartSnapshotBubbleSeries[] }
54
+ | {
55
+ kind: "combo";
56
+ groups: Array<{ kind: "bar" | "line" | "area"; series: ChartSnapshotSeries[] }>;
57
+ categories: string[];
58
+ }
59
+ | { kind: "unsupported"; reason: string };
60
+
61
+ export interface ChartSnapshotSeries {
62
+ name?: string;
63
+ values: Array<number | null>;
64
+ }
65
+
66
+ export interface ChartSnapshotScatterSeries {
67
+ name?: string;
68
+ xValues: Array<number | null>;
69
+ yValues: Array<number | null>;
70
+ }
71
+
72
+ export interface ChartSnapshotBubbleSeries {
73
+ name?: string;
74
+ xValues: Array<number | null>;
75
+ yValues: Array<number | null>;
76
+ sizes: Array<number | null>;
77
+ }
78
+
79
+ export function projectChartSnapshot(chartId: string, model: ChartModel): ChartSnapshot {
80
+ switch (model.kind) {
81
+ case "bar": {
82
+ const m = model as BarChartModel;
83
+ const categories = m.categoryAxis.kind === "category"
84
+ ? m.categoryAxis.categoryLabels
85
+ : (m.series[0]?.categories ?? []);
86
+ return {
87
+ chartId,
88
+ kind: "bar",
89
+ title: m.title?.text,
90
+ seriesCount: m.series.length,
91
+ categoryCount: categories.length,
92
+ data: {
93
+ kind: "bar",
94
+ direction: m.direction,
95
+ grouping: m.grouping,
96
+ series: m.series.map((s) => ({ name: s.name, values: s.values })),
97
+ categories,
98
+ },
99
+ };
100
+ }
101
+ case "line": {
102
+ const m = model as LineChartModel;
103
+ const categories = m.categoryAxis.kind === "category"
104
+ ? m.categoryAxis.categoryLabels
105
+ : (m.series[0]?.categories ?? []);
106
+ return {
107
+ chartId,
108
+ kind: "line",
109
+ title: m.title?.text,
110
+ seriesCount: m.series.length,
111
+ categoryCount: categories.length,
112
+ data: {
113
+ kind: "line",
114
+ grouping: m.grouping,
115
+ series: m.series.map((s) => ({ name: s.name, values: s.values })),
116
+ categories,
117
+ },
118
+ };
119
+ }
120
+ case "pie": {
121
+ const m = model as PieChartModel;
122
+ return {
123
+ chartId,
124
+ kind: "pie",
125
+ title: m.title?.text,
126
+ seriesCount: m.series.length,
127
+ categoryCount: m.categoryLabels.length,
128
+ data: {
129
+ kind: "pie",
130
+ doughnut: m.doughnut,
131
+ series: m.series.map((s) => ({ name: s.name, values: s.values })),
132
+ categories: m.categoryLabels,
133
+ },
134
+ };
135
+ }
136
+ case "area": {
137
+ const m = model as AreaChartModel;
138
+ const categories = m.categoryAxis.kind === "category"
139
+ ? m.categoryAxis.categoryLabels
140
+ : (m.series[0]?.categories ?? []);
141
+ return {
142
+ chartId,
143
+ kind: "area",
144
+ title: m.title?.text,
145
+ seriesCount: m.series.length,
146
+ categoryCount: categories.length,
147
+ data: {
148
+ kind: "area",
149
+ grouping: m.grouping,
150
+ series: m.series.map((s) => ({ name: s.name, values: s.values })),
151
+ categories,
152
+ },
153
+ };
154
+ }
155
+ case "scatter": {
156
+ const m = model as ScatterChartModel;
157
+ return {
158
+ chartId,
159
+ kind: "scatter",
160
+ seriesCount: m.series.length,
161
+ categoryCount: 0,
162
+ data: {
163
+ kind: "scatter",
164
+ series: m.series.map((s) => ({
165
+ name: s.name,
166
+ xValues: s.xValues,
167
+ yValues: s.yValues,
168
+ })),
169
+ },
170
+ };
171
+ }
172
+ case "bubble": {
173
+ const m = model as BubbleChartModel;
174
+ return {
175
+ chartId,
176
+ kind: "bubble",
177
+ seriesCount: m.series.length,
178
+ categoryCount: 0,
179
+ data: {
180
+ kind: "bubble",
181
+ series: m.series.map((s) => ({
182
+ name: s.name,
183
+ xValues: s.xValues,
184
+ yValues: s.yValues,
185
+ sizes: s.sizes,
186
+ })),
187
+ },
188
+ };
189
+ }
190
+ case "combo": {
191
+ const m = model as ComboChartModel;
192
+ const firstGroup = m.groups[0];
193
+ const categories =
194
+ firstGroup && "categoryAxis" in firstGroup &&
195
+ firstGroup.categoryAxis?.kind === "category"
196
+ ? firstGroup.categoryAxis.categoryLabels
197
+ : [];
198
+ return {
199
+ chartId,
200
+ kind: "combo",
201
+ title: m.title?.text,
202
+ seriesCount: m.groups.reduce((acc, g) => acc + g.series.length, 0),
203
+ categoryCount: categories.length,
204
+ data: {
205
+ kind: "combo",
206
+ groups: m.groups.map((g) => ({
207
+ kind: g.kind as "bar" | "line" | "area",
208
+ series: "series" in g
209
+ ? g.series.map((s) => ({
210
+ name: s.name,
211
+ values: "values" in s ? s.values : [],
212
+ }))
213
+ : [],
214
+ })),
215
+ categories,
216
+ },
217
+ };
218
+ }
219
+ case "unsupported": {
220
+ return {
221
+ chartId,
222
+ kind: "unsupported",
223
+ seriesCount: 0,
224
+ categoryCount: 0,
225
+ data: { kind: "unsupported", reason: model.detail },
226
+ };
227
+ }
228
+ default: {
229
+ const _exhaustive: never = model;
230
+ return {
231
+ chartId,
232
+ kind: (_exhaustive as ChartModel).kind,
233
+ seriesCount: 0,
234
+ categoryCount: 0,
235
+ data: { kind: "unsupported", reason: "unknown chart kind" },
236
+ };
237
+ }
238
+ }
239
+ }
@@ -160,7 +160,6 @@ import {
160
160
  createDocumentSectionSnapshots,
161
161
  createSectionLocations,
162
162
  createTocSnapshot,
163
- findBookmarkNameForOffset,
164
163
  findDocumentSectionSnapshot,
165
164
  } from "./document-outline.ts";
166
165
  import {
@@ -210,7 +209,6 @@ import type {
210
209
  BlockNode,
211
210
  FieldNode,
212
211
  FieldRefreshStatus,
213
- HyperlinkNode,
214
212
  InlineNode,
215
213
  PageMargins,
216
214
  ParagraphNode,
@@ -244,6 +242,7 @@ import type {
244
242
  import { collectEditorStateForSerialize } from "./editor-state-integration.ts";
245
243
  import type { EditorStatePayload } from "../io/ooxml/workflow-payload.ts";
246
244
  import type { SharedWorkflowState } from "./collab/workflow-shared.ts";
245
+ import { formatPageNumber } from "./page-number-format.ts";
247
246
 
248
247
  /** Internal extension of ExportDocxOptions that threads the collected
249
248
  * editorState payload from the runtime to the docx serializer. */
@@ -5198,6 +5197,32 @@ function refreshDocumentTableOfContents(
5198
5197
  protectionSelection?: import("../core/state/editor-state.ts").SelectionSnapshot;
5199
5198
  } {
5200
5199
  const navigation = createDocumentNavigationSnapshot(document, selectionHead, activeStory);
5200
+ // Build a single O(N) map from paragraph offset → bookmark name so the
5201
+ // per-heading lookup below is O(1) instead of O(N) per heading.
5202
+ const bookmarkNameByOffset = new Map<number, string>();
5203
+ {
5204
+ let runningOffset = 0;
5205
+ for (const block of document.content.children) {
5206
+ if (block.type !== "paragraph") {
5207
+ continue;
5208
+ }
5209
+ const paragraphStart = runningOffset;
5210
+ for (const child of block.children) {
5211
+ if (child.type === "text") {
5212
+ runningOffset += child.text.length;
5213
+ } else if (child.type === "tab" || child.type === "hard_break") {
5214
+ runningOffset += 1;
5215
+ }
5216
+ }
5217
+ const bookmarkStart = block.children.find(
5218
+ (child): child is Extract<typeof child, { type: "bookmark_start" }> =>
5219
+ child.type === "bookmark_start" && Boolean(child.name),
5220
+ );
5221
+ if (bookmarkStart?.name) {
5222
+ bookmarkNameByOffset.set(paragraphStart, bookmarkStart.name);
5223
+ }
5224
+ }
5225
+ }
5201
5226
  let changed = false;
5202
5227
  let resultEntries: Array<{ level: number; text: string; pageIndex: number; bookmarkName?: string }> = [];
5203
5228
  let changedFrom: number | undefined;
@@ -5212,7 +5237,7 @@ function refreshDocumentTableOfContents(
5212
5237
  const entries = navigation.headings
5213
5238
  .filter((heading) => heading.level >= levelRange.from && heading.level <= levelRange.to)
5214
5239
  .map((heading) => {
5215
- const bookmarkName = findBookmarkNameForOffset(document, heading.offset);
5240
+ const bookmarkName = bookmarkNameByOffset.get(heading.offset);
5216
5241
  return {
5217
5242
  level: heading.level,
5218
5243
  text: heading.text,
@@ -5426,7 +5451,7 @@ function buildTocInlineNodes(
5426
5451
  type: "hyperlink",
5427
5452
  href: `#${entry.bookmarkName}`,
5428
5453
  children: [{ type: "text", text: entry.text }],
5429
- } as HyperlinkNode);
5454
+ });
5430
5455
  } else {
5431
5456
  children.push({ type: "text", text: entry.text });
5432
5457
  }
@@ -5485,6 +5510,38 @@ function collectFieldsFromSubParts(
5485
5510
  return nextIndex;
5486
5511
  }
5487
5512
 
5513
+ function resolveStyleRefFieldText(
5514
+ styleQuery: string,
5515
+ paragraphs: readonly ParagraphContext[],
5516
+ styles: CanonicalDocumentEnvelope["styles"],
5517
+ ): { text: string; refreshStatus: FieldRefreshStatus } {
5518
+ const normalized = styleQuery.trim().toLowerCase();
5519
+
5520
+ // Look up styleId: first by direct id match, then by displayName
5521
+ const styleId = (() => {
5522
+ const byId = Object.keys(styles.paragraphs).find(
5523
+ (id) => id.toLowerCase() === normalized,
5524
+ );
5525
+ if (byId) return byId;
5526
+ const byName = Object.values(styles.paragraphs).find(
5527
+ (s) => s.displayName?.toLowerCase() === normalized,
5528
+ );
5529
+ return byName?.styleId;
5530
+ })();
5531
+
5532
+ if (!styleId) return { text: "", refreshStatus: "unresolvable" };
5533
+
5534
+ // Walk paragraphs top-down for the first matching styleId
5535
+ for (const ctx of paragraphs) {
5536
+ if (ctx.paragraph.styleId === styleId) {
5537
+ const text = flattenInlineDisplayText(ctx.paragraph.children);
5538
+ if (text.trim()) return { text: text.trim(), refreshStatus: "current" };
5539
+ }
5540
+ }
5541
+
5542
+ return { text: "", refreshStatus: "unresolvable" };
5543
+ }
5544
+
5488
5545
  function resolveSupportedFieldDisplay(
5489
5546
  field: FieldNode,
5490
5547
  document: CanonicalDocumentEnvelope,
@@ -5499,13 +5556,30 @@ function resolveSupportedFieldDisplay(
5499
5556
  if (field.fieldFamily === "TOC") {
5500
5557
  return undefined;
5501
5558
  }
5559
+ if (field.fieldFamily === "STYLEREF") {
5560
+ if (!field.fieldTarget) return { displayText: "", refreshStatus: "unresolvable" };
5561
+ const result = resolveStyleRefFieldText(field.fieldTarget, paragraphs, document.styles);
5562
+ return { displayText: result.text, refreshStatus: result.refreshStatus };
5563
+ }
5564
+ if (field.fieldFamily === "SECTIONPAGES") {
5565
+ const sectionIndex = "sectionIndex" in storyTarget && typeof storyTarget.sectionIndex === "number"
5566
+ ? storyTarget.sectionIndex
5567
+ : navigation.activeSectionIndex;
5568
+ const sectionPages = navigation.pages.filter((p) => p.sectionIndex === sectionIndex);
5569
+ if (sectionPages.length === 0) return { displayText: "", refreshStatus: "unresolvable" };
5570
+ const fmt = sectionPages[0]?.layout.pageNumbering?.format;
5571
+ return {
5572
+ displayText: formatPageNumber(sectionPages.length, fmt),
5573
+ refreshStatus: "current",
5574
+ };
5575
+ }
5502
5576
  if (field.fieldFamily === "PAGE") {
5503
5577
  const page = resolveRepresentativePageForStory(navigation, storyTarget);
5504
5578
  if (!page) {
5505
5579
  return { displayText: "", refreshStatus: "unresolvable" };
5506
5580
  }
5507
5581
  return {
5508
- displayText: String(resolveDisplayedPageNumber(page)),
5582
+ displayText: resolveDisplayedPageNumber(page),
5509
5583
  refreshStatus: "current",
5510
5584
  };
5511
5585
  }
@@ -5536,10 +5610,23 @@ function resolveSupportedFieldDisplay(
5536
5610
  if (!paragraph) {
5537
5611
  return { displayText: "", refreshStatus: "unresolvable" };
5538
5612
  }
5613
+
5614
+ // \p switch: emit relative position text ("above" / "below" / "on this page")
5615
+ if (field.switches?.relativePosition === true) {
5616
+ const fieldPage = resolveRepresentativePageForStory(navigation, storyTarget);
5617
+ const targetPageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
5618
+ const fieldPageIndex = fieldPage
5619
+ ? navigation.pages.indexOf(fieldPage)
5620
+ : navigation.activePageIndex;
5621
+ if (targetPageIndex < fieldPageIndex) return { displayText: "above", refreshStatus: "current" };
5622
+ if (targetPageIndex > fieldPageIndex) return { displayText: "below", refreshStatus: "current" };
5623
+ return { displayText: "on this page", refreshStatus: "current" };
5624
+ }
5625
+
5539
5626
  const pageIndex = findPageForOffset(navigation.pages, paragraph.startOffset);
5540
5627
  const page = navigation.pages[pageIndex] ?? navigation.pages[0];
5541
5628
  return page
5542
- ? { displayText: String(resolveDisplayedPageNumber(page)), refreshStatus: "current" }
5629
+ ? { displayText: resolveDisplayedPageNumber(page), refreshStatus: "current" }
5543
5630
  : { displayText: "", refreshStatus: "unresolvable" };
5544
5631
  }
5545
5632
  if (field.fieldFamily === "NOTEREF") {
@@ -5598,8 +5685,9 @@ function isDefaultHeaderFooterPage(
5598
5685
 
5599
5686
  function resolveDisplayedPageNumber(
5600
5687
  page: DocumentNavigationSnapshot["pages"][number],
5601
- ): number {
5602
- return (page.layout.pageNumbering?.start ?? 1) + page.pageInSection;
5688
+ ): string {
5689
+ const n = (page.layout.pageNumbering?.start ?? 1) + page.pageInSection;
5690
+ return formatPageNumber(n, page.layout.pageNumbering?.format);
5603
5691
  }
5604
5692
 
5605
5693
  interface ParagraphContext {
@@ -0,0 +1,207 @@
1
+ /**
2
+ * OOXML page number format codes (ECMA-376 §17.18.59).
3
+ *
4
+ * This module is intentionally dependency-free so it can be unit-tested
5
+ * without any runtime or model imports.
6
+ */
7
+
8
+ const ROMAN_PAIRS: [number, string][] = [
9
+ [1000, "M"],
10
+ [900, "CM"],
11
+ [500, "D"],
12
+ [400, "CD"],
13
+ [100, "C"],
14
+ [90, "XC"],
15
+ [50, "L"],
16
+ [40, "XL"],
17
+ [10, "X"],
18
+ [9, "IX"],
19
+ [5, "V"],
20
+ [4, "IV"],
21
+ [1, "I"],
22
+ ];
23
+
24
+ function toRoman(n: number): string {
25
+ if (n <= 0 || n >= 4000) {
26
+ return String(n);
27
+ }
28
+ let result = "";
29
+ let remainder = n;
30
+ for (const [value, numeral] of ROMAN_PAIRS) {
31
+ while (remainder >= value) {
32
+ result += numeral;
33
+ remainder -= value;
34
+ }
35
+ }
36
+ return result;
37
+ }
38
+
39
+ function toAlphabetic(n: number): string {
40
+ if (n <= 0) {
41
+ return String(n);
42
+ }
43
+ let result = "";
44
+ let remainder = n;
45
+ while (remainder > 0) {
46
+ remainder -= 1;
47
+ result = String.fromCharCode(65 + (remainder % 26)) + result;
48
+ remainder = Math.floor(remainder / 26);
49
+ }
50
+ return result;
51
+ }
52
+
53
+ function toOrdinal(n: number): string {
54
+ const abs = Math.abs(n);
55
+ const mod100 = abs % 100;
56
+ const mod10 = abs % 10;
57
+ if (mod100 >= 11 && mod100 <= 13) {
58
+ return `${n}th`;
59
+ }
60
+ if (mod10 === 1) return `${n}st`;
61
+ if (mod10 === 2) return `${n}nd`;
62
+ if (mod10 === 3) return `${n}rd`;
63
+ return `${n}th`;
64
+ }
65
+
66
+ const ONES = [
67
+ "",
68
+ "one",
69
+ "two",
70
+ "three",
71
+ "four",
72
+ "five",
73
+ "six",
74
+ "seven",
75
+ "eight",
76
+ "nine",
77
+ "ten",
78
+ "eleven",
79
+ "twelve",
80
+ "thirteen",
81
+ "fourteen",
82
+ "fifteen",
83
+ "sixteen",
84
+ "seventeen",
85
+ "eighteen",
86
+ "nineteen",
87
+ ];
88
+
89
+ const TENS = [
90
+ "",
91
+ "",
92
+ "twenty",
93
+ "thirty",
94
+ "forty",
95
+ "fifty",
96
+ "sixty",
97
+ "seventy",
98
+ "eighty",
99
+ "ninety",
100
+ ];
101
+
102
+ function toCardinalText(n: number): string {
103
+ if (n <= 0 || n >= 100) {
104
+ return String(n);
105
+ }
106
+ if (n < 20) {
107
+ return ONES[n]!;
108
+ }
109
+ const tensWord = TENS[Math.floor(n / 10)]!;
110
+ const onesDigit = n % 10;
111
+ if (onesDigit === 0) {
112
+ return tensWord;
113
+ }
114
+ return `${tensWord}-${ONES[onesDigit]}`;
115
+ }
116
+
117
+ const ORDINAL_IRREGULARS: Record<string, string> = {
118
+ one: "first",
119
+ two: "second",
120
+ three: "third",
121
+ four: "fourth",
122
+ five: "fifth",
123
+ six: "sixth",
124
+ seven: "seventh",
125
+ eight: "eighth",
126
+ nine: "ninth",
127
+ ten: "tenth",
128
+ eleven: "eleventh",
129
+ twelve: "twelfth",
130
+ thirteen: "thirteenth",
131
+ fifteen: "fifteenth",
132
+ twenty: "twentieth",
133
+ thirty: "thirtieth",
134
+ forty: "fortieth",
135
+ fifty: "fiftieth",
136
+ sixty: "sixtieth",
137
+ seventy: "seventieth",
138
+ eighty: "eightieth",
139
+ ninety: "ninetieth",
140
+ };
141
+
142
+ function toOrdinalText(n: number): string {
143
+ if (n <= 0 || n >= 100) {
144
+ return `${n}th`;
145
+ }
146
+ const cardinal = toCardinalText(n);
147
+ // Check if it's a hyphenated compound like "twenty-one"
148
+ const hyphenIdx = cardinal.lastIndexOf("-");
149
+ if (hyphenIdx !== -1) {
150
+ const prefix = cardinal.slice(0, hyphenIdx + 1);
151
+ const suffix = cardinal.slice(hyphenIdx + 1);
152
+ const ordinalSuffix = ORDINAL_IRREGULARS[suffix] ?? `${suffix}th`;
153
+ return `${prefix}${ordinalSuffix}`;
154
+ }
155
+ return ORDINAL_IRREGULARS[cardinal] ?? `${cardinal}th`;
156
+ }
157
+
158
+ const CHICAGO_SYMBOLS = ["*", "†", "‡", "§", "‖", "¶"];
159
+
160
+ function toChicago(n: number): string {
161
+ if (n <= 0) {
162
+ return String(n);
163
+ }
164
+ const symbolCount = CHICAGO_SYMBOLS.length;
165
+ // Which cycle: 1-6 → repeat 1×, 7-12 → repeat 2×, etc.
166
+ const cycleIndex = Math.ceil(n / symbolCount) - 1; // 0-based repeat count
167
+ const symbolIndex = ((n - 1) % symbolCount);
168
+ const symbol = CHICAGO_SYMBOLS[symbolIndex]!;
169
+ return symbol.repeat(cycleIndex + 1);
170
+ }
171
+
172
+ /**
173
+ * Format a page number integer using an OOXML ST_NumberFormat code.
174
+ *
175
+ * @param n - The raw page number (1-based within section).
176
+ * @param format - The OOXML format code from `<w:pgNumType w:fmt="…"/>`.
177
+ * @returns Formatted string for display.
178
+ */
179
+ export function formatPageNumber(n: number, format: string | undefined): string {
180
+ switch (format) {
181
+ case undefined:
182
+ case "decimal":
183
+ return String(n);
184
+ case "upperRoman":
185
+ return toRoman(n);
186
+ case "lowerRoman":
187
+ return toRoman(n).toLowerCase();
188
+ case "upperLetter":
189
+ return toAlphabetic(n);
190
+ case "lowerLetter":
191
+ return toAlphabetic(n).toLowerCase();
192
+ case "none":
193
+ return "";
194
+ case "ordinal":
195
+ return toOrdinal(n);
196
+ case "cardinalText":
197
+ return toCardinalText(n);
198
+ case "ordinalText":
199
+ return toOrdinalText(n);
200
+ case "hex":
201
+ return n.toString(16).toUpperCase();
202
+ case "chicago":
203
+ return toChicago(n);
204
+ default:
205
+ return String(n);
206
+ }
207
+ }
@@ -8,6 +8,11 @@ import type {
8
8
  SurfaceTableRowSnapshot,
9
9
  SurfaceTextMark,
10
10
  } from "../api/public-types";
11
+ import {
12
+ chartModelStore,
13
+ extractChartDimensions,
14
+ stableChartId,
15
+ } from "./chart/chart-model-store.ts";
11
16
  import type {
12
17
  CanonicalDocumentEnvelope,
13
18
  SelectionSnapshot,
@@ -1011,10 +1016,25 @@ function appendInlineSegments(
1011
1016
  });
1012
1017
  return { nextCursor: start + 1, lockedFragmentIds: [node.fragmentId] };
1013
1018
  }
1014
- case "chart_preview":
1019
+ case "chart_preview": {
1020
+ let parsedChartId: string | undefined;
1021
+ if (node.parsedData) {
1022
+ parsedChartId = stableChartId(node.rawXml);
1023
+ if (!chartModelStore.has(parsedChartId)) {
1024
+ const { widthPx, heightPx } = extractChartDimensions(node.rawXml);
1025
+ chartModelStore.set(parsedChartId, {
1026
+ model: node.parsedData,
1027
+ widthPx,
1028
+ heightPx,
1029
+ theme: undefined,
1030
+ });
1031
+ }
1032
+ }
1015
1033
  return appendComplexPreviewSegment(paragraph, node, start, "Embedded chart", createChartDetail(node), {
1016
1034
  previewMediaId: node.previewMediaId,
1035
+ parsedChartId,
1017
1036
  });
1037
+ }
1018
1038
  case "smartart_preview":
1019
1039
  return appendComplexPreviewSegment(paragraph, node, start, "SmartArt diagram", createSmartArtDetail(node), {
1020
1040
  previewMediaId: node.previewMediaId,
@@ -1087,6 +1107,14 @@ function appendInlineSegments(
1087
1107
  node.fieldFamily === "PAGE" ||
1088
1108
  node.fieldFamily === "NUMPAGES";
1089
1109
  if (node.children && node.children.length > 0) {
1110
+ // For REF \h, pass the bookmark as a hyperlink href so child text gets hyperlink styling
1111
+ const refHyperlinkHref =
1112
+ node.fieldFamily === "REF" &&
1113
+ node.switches?.hyperlink === true &&
1114
+ node.fieldTarget
1115
+ ? `#${node.fieldTarget}`
1116
+ : undefined;
1117
+
1090
1118
  let cursor = start;
1091
1119
  const lockedIds: string[] = [];
1092
1120
  for (const child of node.children) {
@@ -1096,7 +1124,7 @@ function appendInlineSegments(
1096
1124
  document,
1097
1125
  cursor,
1098
1126
  promoteSecondaryStoryTextBoxes,
1099
- undefined,
1127
+ refHyperlinkHref ?? hyperlinkHref,
1100
1128
  cullBuild,
1101
1129
  );
1102
1130
  cursor = result.nextCursor;
@@ -1159,7 +1187,7 @@ function appendComplexPreviewSegment(
1159
1187
  start: number,
1160
1188
  label: string,
1161
1189
  detail: string,
1162
- extras: { previewMediaId?: string } = {},
1190
+ extras: { previewMediaId?: string; parsedChartId?: string } = {},
1163
1191
  ): { nextCursor: number; lockedFragmentIds: string[] } {
1164
1192
  paragraph.segments.push({
1165
1193
  segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
@@ -1171,6 +1199,7 @@ function appendComplexPreviewSegment(
1171
1199
  label,
1172
1200
  detail,
1173
1201
  ...(extras.previewMediaId ? { previewMediaId: extras.previewMediaId } : {}),
1202
+ ...(extras.parsedChartId ? { parsedChartId: extras.parsedChartId } : {}),
1174
1203
  state: "locked-preserve-only",
1175
1204
  });
1176
1205
  return { nextCursor: start + 1, lockedFragmentIds: [] };