@beyondwork/docx-react-component 1.0.86 → 1.0.87

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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +49 -0
  3. package/src/api/v3/ui/chrome-composition.ts +2 -11
  4. package/src/api/v3/ui/chrome.ts +6 -8
  5. package/src/index.ts +5 -0
  6. package/src/io/export/serialize-main-document.ts +215 -6
  7. package/src/io/ooxml/parse-drawing.ts +15 -1
  8. package/src/io/ooxml/parse-fields.ts +410 -12
  9. package/src/model/canonical-document.ts +177 -2
  10. package/src/model/layout/page-layout-snapshot.ts +2 -0
  11. package/src/model/layout/runtime-page-graph-types.ts +6 -0
  12. package/src/preservation/store.ts +4 -5
  13. package/src/runtime/document-outline.ts +80 -0
  14. package/src/runtime/document-runtime.ts +338 -13
  15. package/src/runtime/formatting/field/page-number-format.ts +49 -0
  16. package/src/runtime/formatting/field/resolver.ts +61 -40
  17. package/src/runtime/layout/layout-engine-instance.ts +18 -1
  18. package/src/runtime/layout/layout-engine-version.ts +19 -1
  19. package/src/runtime/layout/measurement-backend-canvas.ts +21 -9
  20. package/src/runtime/layout/measurement-backend-empirical.ts +18 -4
  21. package/src/runtime/layout/page-graph.ts +13 -2
  22. package/src/runtime/layout/paginated-layout-engine.ts +440 -117
  23. package/src/runtime/layout/project-block-fragments.ts +87 -4
  24. package/src/runtime/layout/resolve-page-fields.ts +8 -5
  25. package/src/runtime/layout/table-row-split.ts +97 -23
  26. package/src/runtime/surface-projection.ts +227 -27
  27. package/src/shell/session-bootstrap.ts +6 -1
  28. package/src/ui/WordReviewEditor.tsx +8 -0
  29. package/src/ui/editor-surface-controller.tsx +1 -0
  30. package/src/ui/headless/revision-decoration-model.ts +11 -13
  31. package/src/ui-tailwind/chrome/responsive-chrome.ts +2 -2
  32. package/src/ui-tailwind/chrome/tw-alert-banner.tsx +27 -0
  33. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +57 -6
  34. package/src/ui-tailwind/editor-surface/page-slice-util.ts +17 -0
  35. package/src/ui-tailwind/editor-surface/perf-probe.ts +5 -0
  36. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +34 -0
  37. package/src/ui-tailwind/editor-surface/pm-decorations.ts +146 -20
  38. package/src/ui-tailwind/editor-surface/pm-position-map.ts +8 -2
  39. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +41 -44
  40. package/src/ui-tailwind/page-stack/use-visible-block-range.ts +1 -1
  41. package/src/ui-tailwind/review/tw-revision-sidebar.tsx +32 -3
  42. package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +1 -1
  43. package/src/ui-tailwind/tw-review-workspace.tsx +18 -0
@@ -1339,6 +1339,9 @@ export type PreserveOnlyFieldFamily =
1339
1339
  | "SEQ"
1340
1340
  | "INDEX"
1341
1341
  | "TC"
1342
+ | "FORMTEXT"
1343
+ | "FORMCHECKBOX"
1344
+ | "FORMDROPDOWN"
1342
1345
  | "FORMULA"
1343
1346
  | "UNKNOWN";
1344
1347
 
@@ -1424,6 +1427,8 @@ export interface FieldRegistry {
1424
1427
  supported: FieldRegistryEntry[];
1425
1428
  /** Preserve-only field instances cataloged for round-trip safety. */
1426
1429
  preserveOnly: FieldRegistryEntry[];
1430
+ /** First-class TOC regions, including cached field-result rows and generated heading rows. */
1431
+ tocRegions?: TocRegion[];
1427
1432
  /** Generated TOC structure extracted from heading-driven TOC fields. */
1428
1433
  tocStructure?: TocStructure;
1429
1434
  }
@@ -1464,6 +1469,78 @@ export interface TocStructure {
1464
1469
  status: "current" | "stale";
1465
1470
  }
1466
1471
 
1472
+ export interface TocInstructionModel {
1473
+ /** The raw TOC field instruction (e.g. "TOC \\o \"1-3\" \\h"). */
1474
+ raw: string;
1475
+ /** Heading level range selected by \\o, or defaulted to 1-9. */
1476
+ outlineRange: { from: number; to: number };
1477
+ /** \\h — result entries should hyperlink to their anchors. */
1478
+ hyperlink?: boolean;
1479
+ /** \\z — hide tab leader and page numbers in web layout. */
1480
+ hidePageNumbersInWeb?: boolean;
1481
+ /** \\u — include paragraphs with outline levels. */
1482
+ useOutlineLevels?: boolean;
1483
+ /** \\t "Style,Level,..." — style-driven TOC mapping. */
1484
+ styleMap?: TocStyleMapEntry[];
1485
+ /** \\b bookmark — bound source content to a bookmark. */
1486
+ bookmarkName?: string;
1487
+ /** \\c identifier — table-of-figures / sequence identifier. */
1488
+ sequenceIdentifier?: string;
1489
+ /** \\f identifier — TC-entry identifier. */
1490
+ tcIdentifier?: string;
1491
+ /** \\p separator — custom entry/page separator. */
1492
+ entryPageSeparator?: string;
1493
+ /** \\d separator — custom sequence separator. */
1494
+ sequenceSeparator?: string;
1495
+ /** \\n optional range — omit page numbers. */
1496
+ omitPageNumbers?: true | { from: number; to: number };
1497
+ /** Switches parsed but not yet semantically implemented. */
1498
+ unsupportedSwitches?: string[];
1499
+ }
1500
+
1501
+ export interface TocStyleMapEntry {
1502
+ styleName: string;
1503
+ level: number;
1504
+ }
1505
+
1506
+ export interface TocCachedEntry {
1507
+ /** Text extracted from the cached field-result row before the page-number tab. */
1508
+ text: string;
1509
+ /** TOC level inferred from TOC1/TOC2/etc. paragraph style. */
1510
+ level: number;
1511
+ /** Paragraph index of the cached result row in document order. */
1512
+ paragraphIndex: number;
1513
+ /** Style ID of the cached result paragraph, if available. */
1514
+ styleId?: string;
1515
+ /** Bookmark/hyperlink anchor used by the cached result row, if present. */
1516
+ bookmarkName?: string;
1517
+ /** Page label extracted from the cached field result. */
1518
+ pageText?: string;
1519
+ /** Full visible row text, including tabs/page labels. */
1520
+ displayText: string;
1521
+ }
1522
+
1523
+ export interface TocRegion {
1524
+ /** Stable document-order id for this imported TOC region. */
1525
+ tocId: string;
1526
+ /** Source TOC field index in FieldRegistry.supported. */
1527
+ sourceFieldIndex: number;
1528
+ /** Parsed instruction metadata and switches. */
1529
+ instruction: TocInstructionModel;
1530
+ /** Inclusive paragraph range containing the cached/generated result rows. */
1531
+ resultRange: { fromParagraphIndex: number; toParagraphIndex: number };
1532
+ /** Parent container kind where the region was found. */
1533
+ parentKind: "body" | "sdt" | "custom_xml" | "table_cell";
1534
+ /** Structural path to the parent block sequence, for diagnostics. */
1535
+ sourcePath: string;
1536
+ /** Cached rows imported from the DOCX field result. */
1537
+ cachedEntries: TocCachedEntry[];
1538
+ /** Heading-derived rows generated from the canonical document. */
1539
+ generatedEntries: TocEntry[];
1540
+ /** Whether cached entries match the generated heading model. */
1541
+ status: "current" | "stale";
1542
+ }
1543
+
1467
1544
  export interface TocEntry {
1468
1545
  /** Heading text. */
1469
1546
  text: string;
@@ -2009,8 +2086,8 @@ export interface DrawingFrameNode {
2009
2086
  content:
2010
2087
  | PictureContent
2011
2088
  | ShapeContent
2012
- | { type: "chart_preview"; rawXml: string }
2013
- | { type: "smartart_preview"; rawXml: string }
2089
+ | ChartPreviewNode
2090
+ | SmartArtPreviewNode
2014
2091
  | { type: "opaque"; rawXml: string };
2015
2092
  }
2016
2093
 
@@ -4069,6 +4146,9 @@ const PRESERVE_ONLY_FIELD_FAMILIES: ReadonlySet<PreserveOnlyFieldFamily> = new S
4069
4146
  "SEQ",
4070
4147
  "INDEX",
4071
4148
  "TC",
4149
+ "FORMTEXT",
4150
+ "FORMCHECKBOX",
4151
+ "FORMDROPDOWN",
4072
4152
  "FORMULA",
4073
4153
  "UNKNOWN",
4074
4154
  ]);
@@ -4123,6 +4203,16 @@ function validateFieldRegistry(
4123
4203
  if (record.tocStructure !== undefined) {
4124
4204
  validateTocStructure(record.tocStructure, `${path}.tocStructure`, issues);
4125
4205
  }
4206
+
4207
+ if (record.tocRegions !== undefined) {
4208
+ if (!Array.isArray(record.tocRegions)) {
4209
+ issues.push({ path: `${path}.tocRegions`, message: "tocRegions must be an array." });
4210
+ } else {
4211
+ record.tocRegions.forEach((region, index) => {
4212
+ validateTocRegion(region, `${path}.tocRegions[${index}]`, issues);
4213
+ });
4214
+ }
4215
+ }
4126
4216
  }
4127
4217
 
4128
4218
  function validateFieldRegistryEntry(
@@ -4265,6 +4355,91 @@ function validateTocStructure(
4265
4355
  }
4266
4356
  }
4267
4357
 
4358
+ function validateTocRegion(
4359
+ value: unknown,
4360
+ path: string,
4361
+ issues: ModelValidationIssue[],
4362
+ ): void {
4363
+ const record = asPlainObject(value, path, issues);
4364
+ if (!record) {
4365
+ return;
4366
+ }
4367
+
4368
+ expectString(record.tocId, `${path}.tocId`, issues);
4369
+ if (!Number.isInteger(record.sourceFieldIndex) || (record.sourceFieldIndex as number) < 0) {
4370
+ issues.push({
4371
+ path: `${path}.sourceFieldIndex`,
4372
+ message: "sourceFieldIndex must be a non-negative integer.",
4373
+ });
4374
+ }
4375
+
4376
+ const instruction = asPlainObject(record.instruction, `${path}.instruction`, issues);
4377
+ if (instruction) {
4378
+ expectString(instruction.raw, `${path}.instruction.raw`, issues);
4379
+ const outlineRange = asPlainObject(
4380
+ instruction.outlineRange,
4381
+ `${path}.instruction.outlineRange`,
4382
+ issues,
4383
+ );
4384
+ if (outlineRange) {
4385
+ if (!Number.isInteger(outlineRange.from) || (outlineRange.from as number) < 1) {
4386
+ issues.push({
4387
+ path: `${path}.instruction.outlineRange.from`,
4388
+ message: "outlineRange.from must be an integer >= 1.",
4389
+ });
4390
+ }
4391
+ if (!Number.isInteger(outlineRange.to) || (outlineRange.to as number) < 1) {
4392
+ issues.push({
4393
+ path: `${path}.instruction.outlineRange.to`,
4394
+ message: "outlineRange.to must be an integer >= 1.",
4395
+ });
4396
+ }
4397
+ }
4398
+ }
4399
+
4400
+ const resultRange = asPlainObject(record.resultRange, `${path}.resultRange`, issues);
4401
+ if (resultRange) {
4402
+ if (!Number.isInteger(resultRange.fromParagraphIndex)) {
4403
+ issues.push({
4404
+ path: `${path}.resultRange.fromParagraphIndex`,
4405
+ message: "fromParagraphIndex must be an integer.",
4406
+ });
4407
+ }
4408
+ if (!Number.isInteger(resultRange.toParagraphIndex)) {
4409
+ issues.push({
4410
+ path: `${path}.resultRange.toParagraphIndex`,
4411
+ message: "toParagraphIndex must be an integer.",
4412
+ });
4413
+ }
4414
+ }
4415
+
4416
+ if (
4417
+ record.parentKind !== "body" &&
4418
+ record.parentKind !== "sdt" &&
4419
+ record.parentKind !== "custom_xml" &&
4420
+ record.parentKind !== "table_cell"
4421
+ ) {
4422
+ issues.push({
4423
+ path: `${path}.parentKind`,
4424
+ message: "parentKind must be body, sdt, custom_xml, or table_cell.",
4425
+ });
4426
+ }
4427
+ expectString(record.sourcePath, `${path}.sourcePath`, issues);
4428
+
4429
+ if (!Array.isArray(record.cachedEntries)) {
4430
+ issues.push({ path: `${path}.cachedEntries`, message: "cachedEntries must be an array." });
4431
+ }
4432
+ if (!Array.isArray(record.generatedEntries)) {
4433
+ issues.push({ path: `${path}.generatedEntries`, message: "generatedEntries must be an array." });
4434
+ }
4435
+ if (record.status !== "current" && record.status !== "stale") {
4436
+ issues.push({
4437
+ path: `${path}.status`,
4438
+ message: "status must be 'current' or 'stale'.",
4439
+ });
4440
+ }
4441
+ }
4442
+
4268
4443
  function validateCanonicalFontTable(
4269
4444
  value: unknown,
4270
4445
  path: string,
@@ -38,6 +38,8 @@ export interface PageLayoutSnapshot {
38
38
  start?: number;
39
39
  chapterStyle?: string;
40
40
  chapterSeparator?: string;
41
+ /** Runtime-derived chapter marker for chapter-prefixed PAGE/PAGEREF display. */
42
+ chapterNumber?: string;
41
43
  };
42
44
  lineNumbering?: {
43
45
  countBy?: number;
@@ -90,6 +90,12 @@ export interface BuildPageGraphInput {
90
90
  >;
91
91
  /** Optional per-page line boxes. */
92
92
  lineBoxes?: ReadonlyMap<string, RuntimeLineBox[]>;
93
+ /**
94
+ * Optional per-page line boxes keyed by pageIndex. This mirrors
95
+ * `fragmentsByPageIndex` for callers that cannot know graph-internal
96
+ * `page-${revision}-${index}` ids before `buildPageGraph` runs.
97
+ */
98
+ lineBoxesByPageIndex?: ReadonlyMap<number, RuntimeLineBox[]>;
93
99
  /** Optional per-page note allocations keyed by graph-assigned pageId. */
94
100
  noteAllocations?: ReadonlyMap<string, RuntimeNoteAllocation[]>;
95
101
  /**
@@ -163,11 +163,10 @@ export function describeStructuredWrapperBlock(
163
163
  ): OpaqueFragmentDescriptor | null {
164
164
  if (block.type === "sdt") {
165
165
  if (isTocContentControl(block)) {
166
- return {
167
- featureKey: "content-controls",
168
- label: "TOC content control",
169
- detail: "TOC content control remains a wrapper-heavy template structure and read-only.",
170
- };
166
+ // CCEP SOW templates wrap the visible cached TOC result paragraphs in a
167
+ // docPartObj SDT. Collapsing that wrapper hides LibreOffice-visible
168
+ // content, so keep the wrapper in the surface tree and project children.
169
+ return null;
171
170
  }
172
171
  if (block.properties.sdtType === "docPartObj") {
173
172
  // coord-02 §11 P0 — a Template content control that wraps a
@@ -211,6 +211,62 @@ export function createTocSnapshot(
211
211
  navigation?: DocumentNavigationSnapshot,
212
212
  ): TocSnapshot | null {
213
213
  const registry = document.fieldRegistry;
214
+ const tocRegions = registry?.tocRegions ?? [];
215
+ const primaryRegion = tocRegions[0];
216
+ if (primaryRegion) {
217
+ const resolvedNavigation =
218
+ navigation ?? createDocumentNavigationSnapshot(document, 0, MAIN_STORY_TARGET);
219
+ const headingRefs = buildHeadingRefs(document, resolvedNavigation);
220
+ const useCachedEntries = primaryRegion.cachedEntries.length > 0;
221
+ const entries: TocEntrySnapshot[] = (useCachedEntries
222
+ ? primaryRegion.cachedEntries
223
+ : primaryRegion.generatedEntries
224
+ ).map((entry, index) => {
225
+ const pageText = useCachedEntries
226
+ ? primaryRegion.cachedEntries[index]?.pageText
227
+ : undefined;
228
+ const heading = useCachedEntries
229
+ ? resolveHeadingForCachedTocEntry(entry, headingRefs)
230
+ : resolveHeadingForTocEntry(primaryRegion.generatedEntries[index]!, headingRefs);
231
+ const bookmarkName =
232
+ entry.bookmarkName ??
233
+ headingRefs.find((ref) => ref.heading.headingId === heading?.headingId)?.bookmarkName;
234
+ return {
235
+ tocEntryId: `${primaryRegion.tocId}-entry-${index}`,
236
+ level: entry.level,
237
+ text: entry.text,
238
+ source: useCachedEntries ? "cached" : "generated",
239
+ ...(pageText ? { pageText } : {}),
240
+ ...(heading
241
+ ? {
242
+ pageIndex: heading.pageIndex,
243
+ anchor: createPublicRangeAnchor(heading.offset, heading.offset),
244
+ headingId: heading.headingId,
245
+ }
246
+ : {}),
247
+ ...(bookmarkName ? { bookmarkName } : {}),
248
+ };
249
+ });
250
+
251
+ return {
252
+ status: primaryRegion.status,
253
+ tocId: primaryRegion.tocId,
254
+ sourceFieldIndex: primaryRegion.sourceFieldIndex,
255
+ instruction: primaryRegion.instruction.raw,
256
+ source: useCachedEntries ? "cached" : "generated",
257
+ regionCount: tocRegions.length,
258
+ regions: tocRegions.map((region) => ({
259
+ tocId: region.tocId,
260
+ status: region.status,
261
+ sourceFieldIndex: region.sourceFieldIndex,
262
+ instruction: region.instruction.raw,
263
+ cachedEntryCount: region.cachedEntries.length,
264
+ generatedEntryCount: region.generatedEntries.length,
265
+ })),
266
+ entries,
267
+ };
268
+ }
269
+
214
270
  const tocStructure = registry?.tocStructure;
215
271
  const fallbackFields = document.fieldRegistry?.supported ?? [];
216
272
  const tocFieldIndex = tocStructure
@@ -331,6 +387,30 @@ function resolveHeadingForTocEntry(
331
387
  return paragraphMatch?.heading;
332
388
  }
333
389
 
390
+ function resolveHeadingForCachedTocEntry(
391
+ entry: { text: string; bookmarkName?: string },
392
+ headingRefs: ReadonlyArray<{
393
+ heading: DocumentHeadingSnapshot;
394
+ paragraphIndex?: number;
395
+ bookmarkName?: string;
396
+ }>,
397
+ ): DocumentHeadingSnapshot | undefined {
398
+ if (entry.bookmarkName) {
399
+ const bookmarkMatch = headingRefs.find((ref) => ref.bookmarkName === entry.bookmarkName);
400
+ if (bookmarkMatch) {
401
+ return bookmarkMatch.heading;
402
+ }
403
+ }
404
+ const normalizedEntryText = normalizeTocSnapshotText(entry.text);
405
+ return headingRefs.find(
406
+ (ref) => normalizeTocSnapshotText(ref.heading.text) === normalizedEntryText,
407
+ )?.heading;
408
+ }
409
+
410
+ function normalizeTocSnapshotText(text: string): string {
411
+ return text.replace(/\s+/gu, " ").trim();
412
+ }
413
+
334
414
  function findFirstTocField(blocks: readonly BlockNode[]): Extract<InlineNode, { type: "field" }> | undefined {
335
415
  for (const block of blocks) {
336
416
  if (block.type === "paragraph") {