@beyondwork/docx-react-component 1.0.58 → 1.0.60
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 +2 -2
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +980 -10
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +4 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +72 -42
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/workflow-payload.ts +6 -1
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +159 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +5 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +153 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +230 -0
- package/src/runtime/document-runtime.ts +821 -54
- package/src/runtime/document-search.ts +115 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +108 -10
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +290 -21
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +187 -43
- package/src/ui/editor-runtime-boundary.ts +10 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +0 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +87 -25
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +9 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +29 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -29,6 +29,8 @@ import type {
|
|
|
29
29
|
SdtNode,
|
|
30
30
|
ShapeNode,
|
|
31
31
|
SmartArtPreviewNode,
|
|
32
|
+
BorderSpec,
|
|
33
|
+
TableBorders,
|
|
32
34
|
TableCellBorders,
|
|
33
35
|
TableNode,
|
|
34
36
|
TextMark,
|
|
@@ -65,6 +67,14 @@ import { resolveHyperlinkRunFormatting } from "./hyperlink-color-resolver.ts";
|
|
|
65
67
|
import { concretizeThemeColors, ThemeColorResolver } from "./theme-color-resolver.ts";
|
|
66
68
|
import type { CanonicalParagraphFormatting, CanonicalRunFormatting } from "../model/canonical-document.ts";
|
|
67
69
|
|
|
70
|
+
const SAFE_CSS_HEX_COLOR_RE = /^#?(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
|
|
71
|
+
const PICTURE_EFFECT_SCHEME_ALIASES: Record<string, string> = {
|
|
72
|
+
tx1: "dk1",
|
|
73
|
+
bg1: "lt1",
|
|
74
|
+
tx2: "dk2",
|
|
75
|
+
bg2: "lt2",
|
|
76
|
+
};
|
|
77
|
+
|
|
68
78
|
interface ParagraphAccumulator {
|
|
69
79
|
blockId: string;
|
|
70
80
|
kind: "paragraph";
|
|
@@ -399,10 +409,37 @@ function createTableBlock(
|
|
|
399
409
|
const rows: SurfaceTableRowSnapshot[] = [];
|
|
400
410
|
const rowSpans = computeTableRowSpans(table);
|
|
401
411
|
const resolvedTable = resolveTableStyleResolution(table, document.styles.tables);
|
|
412
|
+
// SOW gap G3 — build the theme color resolver once per table so every cell
|
|
413
|
+
// shading with `w:themeFill` can resolve the theme slot + tint/shade without
|
|
414
|
+
// reconstructing the resolver per cell.
|
|
415
|
+
const tableThemeResolver = document.subParts?.canonicalTheme
|
|
416
|
+
? new ThemeColorResolver(document.subParts.canonicalTheme)
|
|
417
|
+
: undefined;
|
|
418
|
+
|
|
419
|
+
// SOW gap G5 — fold table-level borders into per-cell rendering. The CCEP
|
|
420
|
+
// SOW form tables ship only `w:tblBorders` (single sz=4 on every side + insideH
|
|
421
|
+
// + insideV) and no per-cell borders, so every interior edge must fall back
|
|
422
|
+
// to `insideH`/`insideV` and perimeter edges must fall back to outer sides.
|
|
423
|
+
const tableBorders = resolvedTable.tableResolved.borders ?? table.borders;
|
|
424
|
+
const totalColumns =
|
|
425
|
+
table.gridColumns.length > 0
|
|
426
|
+
? table.gridColumns.length
|
|
427
|
+
: table.rows.reduce(
|
|
428
|
+
(max, r) =>
|
|
429
|
+
Math.max(
|
|
430
|
+
max,
|
|
431
|
+
(r.gridBefore ?? 0) +
|
|
432
|
+
r.cells.reduce((sum, c) => sum + (c.gridSpan ?? 1), 0) +
|
|
433
|
+
(r.gridAfter ?? 0),
|
|
434
|
+
),
|
|
435
|
+
0,
|
|
436
|
+
);
|
|
437
|
+
const totalRows = table.rows.length;
|
|
402
438
|
|
|
403
439
|
for (const [rowIndex, row] of table.rows.entries()) {
|
|
404
440
|
const cells: SurfaceTableCellSnapshot[] = [];
|
|
405
441
|
const resolvedRow = resolvedTable.rows[rowIndex];
|
|
442
|
+
let columnCursor = row.gridBefore ?? 0;
|
|
406
443
|
for (const [cellIndex, cell] of row.cells.entries()) {
|
|
407
444
|
const cellContent: SurfaceBlockSnapshot[] = [];
|
|
408
445
|
for (const child of cell.children) {
|
|
@@ -420,7 +457,30 @@ function createTableBlock(
|
|
|
420
457
|
innerCursor = result.nextCursor;
|
|
421
458
|
}
|
|
422
459
|
const resolvedCell = resolvedRow?.cells[cellIndex];
|
|
423
|
-
const
|
|
460
|
+
const cellSpan = cell.gridSpan ?? 1;
|
|
461
|
+
const startColumn = columnCursor;
|
|
462
|
+
const endColumn = columnCursor + cellSpan - 1;
|
|
463
|
+
const position = {
|
|
464
|
+
isTopEdge: rowIndex === 0,
|
|
465
|
+
isBottomEdge: rowIndex === totalRows - 1,
|
|
466
|
+
isLeftEdge: startColumn === 0,
|
|
467
|
+
isRightEdge: endColumn === totalColumns - 1,
|
|
468
|
+
};
|
|
469
|
+
columnCursor = endColumn + 1;
|
|
470
|
+
const cellBorders = resolveCellBorderStyles(
|
|
471
|
+
resolvedCell?.borders ?? cell.borders,
|
|
472
|
+
tableBorders,
|
|
473
|
+
position,
|
|
474
|
+
);
|
|
475
|
+
// SOW gap G3 — resolve effective cell fill honoring theme references.
|
|
476
|
+
// `w:themeFill` wins when `w:fill` is absent or "auto"; the resolver
|
|
477
|
+
// applies `w:clrSchemeMapping` remap + byte-form tint/shade from
|
|
478
|
+
// `w:themeFillTint` / `w:themeFillShade`. Returned hex has no leading
|
|
479
|
+
// "#" to match the direct-fill convention below.
|
|
480
|
+
const effectiveFill = resolveCellShadingFill(
|
|
481
|
+
resolvedCell?.shading ?? cell.shading,
|
|
482
|
+
tableThemeResolver,
|
|
483
|
+
);
|
|
424
484
|
// R2a: project the resolved conditional-format regions into CSS class
|
|
425
485
|
// names so the NodeView can paint band colors via theme vars instead of
|
|
426
486
|
// inline styles. Direct shading overrides still win at render time.
|
|
@@ -436,7 +496,7 @@ function createTableBlock(
|
|
|
436
496
|
verticalMerge: cell.verticalMerge ?? null,
|
|
437
497
|
colspan: cell.gridSpan ?? 1,
|
|
438
498
|
rowspan: rowSpans.get(`${rowIndex}:${cellIndex}`) ?? 1,
|
|
439
|
-
...(
|
|
499
|
+
...(effectiveFill ? { backgroundColor: `#${effectiveFill}` } : {}),
|
|
440
500
|
...(resolvedCell?.verticalAlign ? { verticalAlign: resolvedCell.verticalAlign } : {}),
|
|
441
501
|
...(cellBorders.borderTop ? { borderTop: cellBorders.borderTop } : {}),
|
|
442
502
|
...(cellBorders.borderRight ? { borderRight: cellBorders.borderRight } : {}),
|
|
@@ -487,6 +547,17 @@ function createTableBlock(
|
|
|
487
547
|
}
|
|
488
548
|
: undefined;
|
|
489
549
|
|
|
550
|
+
// SOW gap G1 — when the table's own width is expressed as a percent
|
|
551
|
+
// (`w:tblW w:type="pct"`), compute relative column proportions so the
|
|
552
|
+
// node-view can emit `<col style="width: NN%">` instead of absolute `pt`
|
|
553
|
+
// widths. This matters for the CCEP SOW, whose tables all use pct widths
|
|
554
|
+
// at both table and cell level: absolute pt columns inside a `width:100%`
|
|
555
|
+
// container drift against the container on zoom + re-layout.
|
|
556
|
+
const gridColumnsRelative = computeRelativeGridColumns(
|
|
557
|
+
table.gridColumns,
|
|
558
|
+
tr.widthType,
|
|
559
|
+
);
|
|
560
|
+
|
|
490
561
|
return {
|
|
491
562
|
block: {
|
|
492
563
|
blockId: `table-${tableIndex}`,
|
|
@@ -495,6 +566,7 @@ function createTableBlock(
|
|
|
495
566
|
to: innerCursor,
|
|
496
567
|
styleId: table.styleId,
|
|
497
568
|
gridColumns: table.gridColumns,
|
|
569
|
+
...(gridColumnsRelative ? { gridColumnsRelative } : {}),
|
|
498
570
|
...(resolvedTable.table?.alignment ? { alignment: resolvedTable.table.alignment } : {}),
|
|
499
571
|
tblLook: resolvedTable.effectiveTblLook,
|
|
500
572
|
...(tableResolvedAttr ? { tableResolved: tableResolvedAttr } : {}),
|
|
@@ -554,20 +626,148 @@ function computeTableRowSpans(table: TableNode): Map<string, number> {
|
|
|
554
626
|
return rowSpans;
|
|
555
627
|
}
|
|
556
628
|
|
|
629
|
+
/**
|
|
630
|
+
* SOW gap G3 — resolve the final paintable fill for a cell `w:shd`. Precedence
|
|
631
|
+
* matches Word: a concrete `w:fill` (non-"auto") wins; otherwise the theme
|
|
632
|
+
* reference in `w:themeFill` + `w:themeFillTint` / `w:themeFillShade` is
|
|
633
|
+
* resolved through the theme cascade. Returns a 6-digit hex WITHOUT a leading
|
|
634
|
+
* "#" to match the direct-fill convention, or undefined when no paintable
|
|
635
|
+
* color can be derived.
|
|
636
|
+
*/
|
|
637
|
+
function resolveCellShadingFill(
|
|
638
|
+
shading:
|
|
639
|
+
| {
|
|
640
|
+
fill?: string;
|
|
641
|
+
themeFill?: string;
|
|
642
|
+
themeFillTint?: string;
|
|
643
|
+
themeFillShade?: string;
|
|
644
|
+
}
|
|
645
|
+
| undefined,
|
|
646
|
+
themeResolver: ThemeColorResolver | undefined,
|
|
647
|
+
): string | undefined {
|
|
648
|
+
if (!shading) return undefined;
|
|
649
|
+
const direct = shading.fill;
|
|
650
|
+
if (direct && direct !== "auto") return direct;
|
|
651
|
+
const themeSlot = shading.themeFill;
|
|
652
|
+
if (!themeSlot || !themeResolver) return direct; // keep "auto" fallback if present
|
|
653
|
+
const resolved = themeResolver.resolveWordThemeColor(
|
|
654
|
+
themeSlot,
|
|
655
|
+
shading.themeFillTint,
|
|
656
|
+
shading.themeFillShade,
|
|
657
|
+
);
|
|
658
|
+
if (!resolved || resolved === "auto") return direct;
|
|
659
|
+
// Strip leading "#" if the resolver returned one.
|
|
660
|
+
return resolved.startsWith("#") ? resolved.slice(1) : resolved;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* SOW gap G1 — derive relative column widths (percent 0–100, sum 100)
|
|
665
|
+
* from the canonical `gridColumns` (twips). Only populated when the table
|
|
666
|
+
* itself is sized in percent; other width types keep the absolute pt path
|
|
667
|
+
* in the node-view. Returns null when the grid is empty, all-zero, or the
|
|
668
|
+
* width type is not `pct`.
|
|
669
|
+
*/
|
|
670
|
+
function computeRelativeGridColumns(
|
|
671
|
+
gridColumns: readonly number[],
|
|
672
|
+
widthType: "auto" | "dxa" | "pct" | "nil" | undefined,
|
|
673
|
+
): number[] | null {
|
|
674
|
+
if (widthType !== "pct") return null;
|
|
675
|
+
if (gridColumns.length === 0) return null;
|
|
676
|
+
let total = 0;
|
|
677
|
+
for (const col of gridColumns) total += col > 0 ? col : 0;
|
|
678
|
+
if (total <= 0) return null;
|
|
679
|
+
return gridColumns.map((col) => (col > 0 ? (col / total) * 100 : 0));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Render a typed `BorderSpec` shape to a CSS shorthand string. Returns
|
|
684
|
+
* undefined for absent / "none" / "nil" specs so callers can keep their
|
|
685
|
+
* falsy-check pattern.
|
|
686
|
+
*/
|
|
687
|
+
function borderSpecToCssShorthand(
|
|
688
|
+
spec: BorderSpec | undefined | null,
|
|
689
|
+
): string | undefined {
|
|
690
|
+
if (!spec) return undefined;
|
|
691
|
+
if (spec.value === "none" || spec.value === "nil") return undefined;
|
|
692
|
+
const width = spec.size ? `${Math.max(1, Math.round(spec.size / 8))}px` : "1px";
|
|
693
|
+
const style =
|
|
694
|
+
spec.value === "double"
|
|
695
|
+
? "double"
|
|
696
|
+
: spec.value === "dashed" || spec.value === "dashSmallGap"
|
|
697
|
+
? "dashed"
|
|
698
|
+
: spec.value === "dotted"
|
|
699
|
+
? "dotted"
|
|
700
|
+
: "solid";
|
|
701
|
+
const color = spec.color && spec.color !== "auto" ? `#${spec.color}` : "currentColor";
|
|
702
|
+
return `${width} ${style} ${color}`;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* SOW gap G5 — border cascade for an individual cell. When the cell does not
|
|
707
|
+
* declare `w:tcBorders.<side>`, the renderer falls back to the table-level
|
|
708
|
+
* `w:tblBorders`: outer sides (`top`/`right`/`bottom`/`left`) apply when the
|
|
709
|
+
* cell sits on the corresponding table perimeter; `insideH`/`insideV` apply
|
|
710
|
+
* to interior edges. The CCEP SOW relies on this cascade — its form tables
|
|
711
|
+
* declare only `w:tblBorders` and expect every cell to carry visible 1px
|
|
712
|
+
* borders on all four sides.
|
|
713
|
+
*
|
|
714
|
+
* `position` is optional; when absent, the function falls back to the legacy
|
|
715
|
+
* cell-only behavior (no inside/outer cascade) so older callers keep working.
|
|
716
|
+
*/
|
|
557
717
|
function resolveCellBorderStyles(
|
|
558
718
|
borders: TableCellBorders | undefined,
|
|
719
|
+
tableBorders?: TableBorders,
|
|
720
|
+
position?: {
|
|
721
|
+
isTopEdge: boolean;
|
|
722
|
+
isBottomEdge: boolean;
|
|
723
|
+
isLeftEdge: boolean;
|
|
724
|
+
isRightEdge: boolean;
|
|
725
|
+
},
|
|
559
726
|
): { borderTop?: string; borderRight?: string; borderBottom?: string; borderLeft?: string } {
|
|
560
|
-
|
|
727
|
+
const pick = (
|
|
728
|
+
cellSide: BorderSpec | undefined,
|
|
729
|
+
tableOuter: BorderSpec | undefined,
|
|
730
|
+
tableInside: BorderSpec | undefined,
|
|
731
|
+
onEdge: boolean,
|
|
732
|
+
): string | undefined => {
|
|
733
|
+
if (cellSide && cellSide.value !== "nil") {
|
|
734
|
+
const rendered = borderSpecToCssShorthand(cellSide);
|
|
735
|
+
if (rendered) return rendered;
|
|
736
|
+
if (cellSide.value === "none") return undefined; // explicit "off" — no fallback
|
|
737
|
+
}
|
|
738
|
+
const fallback = onEdge ? tableOuter : tableInside;
|
|
739
|
+
return borderSpecToCssShorthand(fallback);
|
|
740
|
+
};
|
|
741
|
+
|
|
561
742
|
const result: { borderTop?: string; borderRight?: string; borderBottom?: string; borderLeft?: string } = {};
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
743
|
+
const top = pick(
|
|
744
|
+
borders?.top,
|
|
745
|
+
tableBorders?.top,
|
|
746
|
+
tableBorders?.insideH,
|
|
747
|
+
position?.isTopEdge ?? true,
|
|
748
|
+
);
|
|
749
|
+
const right = pick(
|
|
750
|
+
borders?.right,
|
|
751
|
+
tableBorders?.right,
|
|
752
|
+
tableBorders?.insideV,
|
|
753
|
+
position?.isRightEdge ?? true,
|
|
754
|
+
);
|
|
755
|
+
const bottom = pick(
|
|
756
|
+
borders?.bottom,
|
|
757
|
+
tableBorders?.bottom,
|
|
758
|
+
tableBorders?.insideH,
|
|
759
|
+
position?.isBottomEdge ?? true,
|
|
760
|
+
);
|
|
761
|
+
const left = pick(
|
|
762
|
+
borders?.left,
|
|
763
|
+
tableBorders?.left,
|
|
764
|
+
tableBorders?.insideV,
|
|
765
|
+
position?.isLeftEdge ?? true,
|
|
766
|
+
);
|
|
767
|
+
if (top) result.borderTop = top;
|
|
768
|
+
if (right) result.borderRight = right;
|
|
769
|
+
if (bottom) result.borderBottom = bottom;
|
|
770
|
+
if (left) result.borderLeft = left;
|
|
571
771
|
return result;
|
|
572
772
|
}
|
|
573
773
|
|
|
@@ -651,13 +851,14 @@ function createParagraphBlock(
|
|
|
651
851
|
// by the placeholder path. Segment-level work inside
|
|
652
852
|
// `appendInlineSegments` is suppressed symmetrically via the same
|
|
653
853
|
// `cullBuild` flag, preserving cursor arithmetic.
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
: resolveEffectiveParagraphNumbering(document, paragraph);
|
|
854
|
+
// Always resolve numbering so the counter advances for culled paragraphs too.
|
|
855
|
+
const effectiveNumbering = resolveEffectiveParagraphNumbering(document, paragraph);
|
|
657
856
|
const resolvedNumbering =
|
|
658
857
|
!cullBuild && effectiveNumbering
|
|
659
858
|
? numberingPrefixResolver.resolveDetailed(effectiveNumbering, paragraph)
|
|
660
|
-
:
|
|
859
|
+
: effectiveNumbering
|
|
860
|
+
? advanceNumberingCounterOnly(numberingPrefixResolver, effectiveNumbering)
|
|
861
|
+
: null;
|
|
661
862
|
|
|
662
863
|
// Task 11: compute cascaded paragraph formatting (expensive — styles-catalog walk).
|
|
663
864
|
const stylesCatalog = document.styles;
|
|
@@ -760,6 +961,14 @@ function createParagraphBlock(
|
|
|
760
961
|
};
|
|
761
962
|
}
|
|
762
963
|
|
|
964
|
+
function advanceNumberingCounterOnly(
|
|
965
|
+
resolver: NumberingPrefixResolver,
|
|
966
|
+
numbering: NonNullable<ParagraphNode["numbering"]>,
|
|
967
|
+
): null {
|
|
968
|
+
resolver.resolve(numbering);
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
971
|
+
|
|
763
972
|
function resolveEffectiveParagraphNumbering(
|
|
764
973
|
document: CanonicalDocumentEnvelope,
|
|
765
974
|
paragraph: ParagraphNode,
|
|
@@ -1100,7 +1309,7 @@ function appendInlineSegments(
|
|
|
1100
1309
|
const mediaId = c.mediaId ?? `drawing-frame-${start}`;
|
|
1101
1310
|
const state: "editable" | "missing" = c.mediaId ? "editable" : "missing";
|
|
1102
1311
|
const anchor = surfaceAnchorFromGeometry(node.anchor);
|
|
1103
|
-
const pictureEffects = surfacePictureEffectsFromContent(c);
|
|
1312
|
+
const pictureEffects = surfacePictureEffectsFromContent(c, themeResolver);
|
|
1104
1313
|
paragraph.segments.push({
|
|
1105
1314
|
segmentId: `${paragraph.blockId}-segment-${paragraph.segments.length}`,
|
|
1106
1315
|
kind: "image",
|
|
@@ -1303,6 +1512,7 @@ function surfaceAnchorFromGeometry(
|
|
|
1303
1512
|
...(anchor.allowOverlap !== undefined ? { allowOverlap: anchor.allowOverlap } : {}),
|
|
1304
1513
|
...(anchor.simplePos !== undefined ? { simplePos: anchor.simplePos } : {}),
|
|
1305
1514
|
...(anchor.docPr ? { docPr: { ...anchor.docPr } } : {}),
|
|
1515
|
+
...(anchor.wrapPolygon ? { wrapPolygon: anchor.wrapPolygon } : {}),
|
|
1306
1516
|
};
|
|
1307
1517
|
}
|
|
1308
1518
|
|
|
@@ -1313,7 +1523,10 @@ function surfaceAnchorFromGeometry(
|
|
|
1313
1523
|
*/
|
|
1314
1524
|
function surfacePictureEffectsFromContent(
|
|
1315
1525
|
content: PictureContent,
|
|
1526
|
+
themeResolver?: ThemeColorResolver,
|
|
1316
1527
|
): SurfacePictureEffects | undefined {
|
|
1528
|
+
const outerShadow = resolveSurfacePictureShadow(content.outerShadow, themeResolver);
|
|
1529
|
+
const glow = resolveSurfacePictureGlow(content.glow, themeResolver);
|
|
1317
1530
|
const has =
|
|
1318
1531
|
content.srcRect !== undefined ||
|
|
1319
1532
|
content.rotation !== undefined ||
|
|
@@ -1322,8 +1535,8 @@ function surfacePictureEffectsFromContent(
|
|
|
1322
1535
|
content.presetGeom !== undefined ||
|
|
1323
1536
|
content.stretch !== undefined ||
|
|
1324
1537
|
content.softEdgeRadius !== undefined ||
|
|
1325
|
-
|
|
1326
|
-
|
|
1538
|
+
outerShadow !== undefined ||
|
|
1539
|
+
glow !== undefined;
|
|
1327
1540
|
if (!has) return undefined;
|
|
1328
1541
|
return {
|
|
1329
1542
|
...(content.srcRect ? { srcRect: { ...content.srcRect } } : {}),
|
|
@@ -1333,11 +1546,62 @@ function surfacePictureEffectsFromContent(
|
|
|
1333
1546
|
...(content.presetGeom !== undefined ? { presetGeom: content.presetGeom } : {}),
|
|
1334
1547
|
...(content.stretch !== undefined ? { stretch: content.stretch } : {}),
|
|
1335
1548
|
...(content.softEdgeRadius !== undefined ? { softEdgeRadius: content.softEdgeRadius } : {}),
|
|
1336
|
-
...(
|
|
1337
|
-
...(
|
|
1549
|
+
...(outerShadow ? { outerShadow } : {}),
|
|
1550
|
+
...(glow ? { glow } : {}),
|
|
1338
1551
|
};
|
|
1339
1552
|
}
|
|
1340
1553
|
|
|
1554
|
+
function resolveSurfacePictureShadow(
|
|
1555
|
+
shadow: PictureContent["outerShadow"] | undefined,
|
|
1556
|
+
themeResolver?: ThemeColorResolver,
|
|
1557
|
+
): SurfacePictureEffects["outerShadow"] | undefined {
|
|
1558
|
+
if (!shadow) return undefined;
|
|
1559
|
+
const color = resolveSurfacePictureEffectColor(shadow.color, shadow.colorType, themeResolver);
|
|
1560
|
+
if (!color) return undefined;
|
|
1561
|
+
return {
|
|
1562
|
+
blurRad: shadow.blurRad,
|
|
1563
|
+
dist: shadow.dist,
|
|
1564
|
+
dir: shadow.dir,
|
|
1565
|
+
color,
|
|
1566
|
+
colorType: "srgbClr",
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
function resolveSurfacePictureGlow(
|
|
1571
|
+
glow: PictureContent["glow"] | undefined,
|
|
1572
|
+
themeResolver?: ThemeColorResolver,
|
|
1573
|
+
): SurfacePictureEffects["glow"] | undefined {
|
|
1574
|
+
if (!glow) return undefined;
|
|
1575
|
+
const color = resolveSurfacePictureEffectColor(glow.color, glow.colorType, themeResolver);
|
|
1576
|
+
if (!color) return undefined;
|
|
1577
|
+
return {
|
|
1578
|
+
radius: glow.radius,
|
|
1579
|
+
color,
|
|
1580
|
+
colorType: "srgbClr",
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function resolveSurfacePictureEffectColor(
|
|
1585
|
+
color: string,
|
|
1586
|
+
colorType: "srgbClr" | "schemeClr",
|
|
1587
|
+
themeResolver?: ThemeColorResolver,
|
|
1588
|
+
): string | undefined {
|
|
1589
|
+
if (colorType === "srgbClr") {
|
|
1590
|
+
return normalizeSafeCssHexColor(color);
|
|
1591
|
+
}
|
|
1592
|
+
const slot = PICTURE_EFFECT_SCHEME_ALIASES[color] ?? color;
|
|
1593
|
+
if (slot === "phClr") return undefined;
|
|
1594
|
+
const resolved = themeResolver?.resolveSchemeSlot(slot);
|
|
1595
|
+
return normalizeSafeCssHexColor(resolved);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function normalizeSafeCssHexColor(value: string | undefined): string | undefined {
|
|
1599
|
+
if (!value) return undefined;
|
|
1600
|
+
const trimmed = value.trim();
|
|
1601
|
+
if (!SAFE_CSS_HEX_COLOR_RE.test(trimmed)) return undefined;
|
|
1602
|
+
return trimmed.replace(/^#/, "").toUpperCase();
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1341
1605
|
/**
|
|
1342
1606
|
* V2c.5 — Extract the first paragraph's plain text from a parsed
|
|
1343
1607
|
* `txbxBlocks` tree for the `txbxText` segment preview. The recursion
|
|
@@ -1796,6 +2060,10 @@ function summarizePreviewInline(node: InlineNode): string {
|
|
|
1796
2060
|
return node.text ? `[VML: ${node.text}]` : "[Legacy VML drawing]";
|
|
1797
2061
|
case "drawing_frame":
|
|
1798
2062
|
return node.content.type === "picture" ? "[Image]" : "[Drawing]";
|
|
2063
|
+
case "ole_embed":
|
|
2064
|
+
return node.progId
|
|
2065
|
+
? `[Embedded object: ${node.progId}]`
|
|
2066
|
+
: "[Embedded object]";
|
|
1799
2067
|
}
|
|
1800
2068
|
}
|
|
1801
2069
|
|
|
@@ -1866,6 +2134,7 @@ function toSurfaceResolvedNumbering(
|
|
|
1866
2134
|
? { textColumn: { ...numbering.geometry.textColumn } }
|
|
1867
2135
|
: {}),
|
|
1868
2136
|
},
|
|
2137
|
+
...(numbering.picBulletMediaId ? { picBulletMediaId: numbering.picBulletMediaId } : {}),
|
|
1869
2138
|
};
|
|
1870
2139
|
}
|
|
1871
2140
|
|
|
@@ -210,6 +210,12 @@ export const tableNodeSpec: NodeSpec = {
|
|
|
210
210
|
styleId: { default: null },
|
|
211
211
|
propertiesXml: { default: null },
|
|
212
212
|
gridColumns: { default: [] },
|
|
213
|
+
/**
|
|
214
|
+
* SOW gap G1 — relative column widths (percent 0–100) used for the
|
|
215
|
+
* `<colgroup>` when the table width itself is expressed as a percent.
|
|
216
|
+
* `null` = use absolute pt widths from `gridColumns`.
|
|
217
|
+
*/
|
|
218
|
+
gridColumnsRelative: { default: null },
|
|
213
219
|
alignment: { default: null },
|
|
214
220
|
tblLookFirstRow: { default: false },
|
|
215
221
|
tblLookLastRow: { default: false },
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import type { CanonicalRunFormatting, CanonicalTheme, ClrSchemeMappingSlot, ResolvedTheme } from "../model/canonical-document.ts";
|
|
28
28
|
import { resolveThemeColor } from "../io/ooxml/parse-theme.ts";
|
|
29
|
+
import { GRADIENT_STOP_UNITS } from "./units.ts";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* DrawingML color modifier (ECMA-376 §20.1.2.3.x).
|
|
@@ -40,7 +41,6 @@ export interface DrawingMlColorMod {
|
|
|
40
41
|
value: number;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
const DML_UNIT = 100_000;
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Unified runtime theme color resolver.
|
|
@@ -251,7 +251,7 @@ function applyDmlMods(
|
|
|
251
251
|
): string {
|
|
252
252
|
let rgb = parseHexToRgbDml(hex);
|
|
253
253
|
for (const mod of mods) {
|
|
254
|
-
const frac = mod.value /
|
|
254
|
+
const frac = mod.value / GRADIENT_STOP_UNITS;
|
|
255
255
|
switch (mod.kind) {
|
|
256
256
|
case "lumMod": {
|
|
257
257
|
const hsl = rgbToHsl(rgb);
|
package/src/runtime/units.ts
CHANGED
|
@@ -16,9 +16,18 @@
|
|
|
16
16
|
/** EMU (English Metric Units) per CSS pixel at 96 dpi. */
|
|
17
17
|
export const EMU_PER_PX = 9525;
|
|
18
18
|
|
|
19
|
+
/** EMU per inch — 914 400 EMU = 1 inch (ECMA-376 §20.1.2.2). */
|
|
20
|
+
export const EMU_PER_INCH = 914_400;
|
|
21
|
+
|
|
19
22
|
/** OOXML rotation units per degree (`a:xfrm a:rot` = 60 000ths°). */
|
|
20
23
|
export const ROTATION_UNITS_PER_DEGREE = 60000;
|
|
21
24
|
|
|
25
|
+
/** OOXML gradient-stop and crop-offset unit (100 000 = 100%). */
|
|
26
|
+
export const GRADIENT_STOP_UNITS = 100_000;
|
|
27
|
+
|
|
28
|
+
/** OOXML percentage parts unit (5 000 = 100%). Used for table pct widths. */
|
|
29
|
+
export const PERCENTAGE_PARTS = 5_000;
|
|
30
|
+
|
|
22
31
|
/** OOXML picture-crop units per percent (`a:srcRect` uses 1/1000 of a percent). */
|
|
23
32
|
export const SRCRECT_UNITS_PER_PERCENT = 1000;
|
|
24
33
|
|
|
@@ -387,15 +387,18 @@ export function attachScopeCardModel(
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
const workItemByScope = new Map<string, string>();
|
|
390
|
+
const anchorByScope = new Map<string, EditorAnchorProjection>();
|
|
390
391
|
for (const scope of input.scopes ?? []) {
|
|
391
392
|
if (scope.workItemId) {
|
|
392
393
|
workItemByScope.set(scope.scopeId, scope.workItemId);
|
|
393
394
|
}
|
|
395
|
+
anchorByScope.set(scope.scopeId, scope.anchor);
|
|
394
396
|
}
|
|
395
397
|
|
|
396
398
|
const models: ScopeCardModel[] = [];
|
|
397
399
|
for (const segment of firstByScope.values()) {
|
|
398
400
|
const workItemId = workItemByScope.get(segment.scopeId);
|
|
401
|
+
const anchor = anchorByScope.get(segment.scopeId);
|
|
399
402
|
const issue = resolveIssueForScope(
|
|
400
403
|
segment.scopeId,
|
|
401
404
|
workItemId,
|
|
@@ -423,6 +426,7 @@ export function attachScopeCardModel(
|
|
|
423
426
|
label: segment.label ?? "",
|
|
424
427
|
posture: segment.posture,
|
|
425
428
|
primaryAnchorRect,
|
|
429
|
+
...(anchor ? { anchor } : {}),
|
|
426
430
|
...(issue ? { issue } : {}),
|
|
427
431
|
suggestionGroupIds: suggestionGroups.map((group) => group.groupId),
|
|
428
432
|
suggestionGroups,
|