@beyondwork/docx-react-component 1.0.37 → 1.0.39
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 +41 -31
- package/src/api/public-types.ts +496 -1
- package/src/core/commands/section-layout-commands.ts +58 -0
- package/src/core/commands/table-grid.ts +431 -0
- package/src/core/commands/table-structure-commands.ts +845 -56
- package/src/core/commands/text-commands.ts +122 -2
- package/src/io/docx-session.ts +1 -0
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +43 -10
- package/src/io/export/serialize-paragraph-formatting.ts +152 -0
- package/src/io/export/serialize-run-formatting.ts +90 -0
- package/src/io/export/serialize-styles.ts +212 -0
- package/src/io/export/serialize-tables.ts +74 -0
- package/src/io/export/table-properties-xml.ts +139 -4
- package/src/io/normalize/normalize-text.ts +15 -0
- package/src/io/ooxml/parse-fields.ts +10 -3
- package/src/io/ooxml/parse-footnotes.ts +60 -0
- package/src/io/ooxml/parse-headers-footers.ts +60 -0
- package/src/io/ooxml/parse-main-document.ts +137 -0
- package/src/io/ooxml/parse-numbering.ts +41 -1
- package/src/io/ooxml/parse-paragraph-formatting.ts +188 -0
- package/src/io/ooxml/parse-run-formatting.ts +129 -0
- package/src/io/ooxml/parse-styles.ts +31 -0
- package/src/io/ooxml/parse-tables.ts +249 -0
- package/src/io/ooxml/xml-attr-helpers.ts +60 -0
- package/src/io/ooxml/xml-element.ts +19 -0
- package/src/model/canonical-document.ts +117 -3
- package/src/runtime/collab/event-types.ts +165 -0
- package/src/runtime/collab/index.ts +22 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +93 -0
- package/src/runtime/collab/runtime-collab-sync.ts +273 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +248 -18
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +47 -0
- package/src/runtime/layout/inert-layout-facet.ts +16 -0
- package/src/runtime/layout/layout-engine-instance.ts +100 -23
- package/src/runtime/layout/layout-invalidation.ts +14 -5
- package/src/runtime/layout/margin-preset-catalog.ts +178 -0
- package/src/runtime/layout/page-format-catalog.ts +233 -0
- package/src/runtime/layout/page-graph.ts +55 -0
- package/src/runtime/layout/paginate-paragraph-lines.ts +128 -0
- package/src/runtime/layout/paginated-layout-engine.ts +484 -37
- package/src/runtime/layout/project-block-fragments.ts +225 -0
- package/src/runtime/layout/public-facet.ts +748 -16
- package/src/runtime/layout/resolve-page-fields.ts +70 -0
- package/src/runtime/layout/resolve-page-previews.ts +185 -0
- package/src/runtime/layout/resolved-formatting-state.ts +30 -26
- package/src/runtime/layout/table-render-plan.ts +249 -0
- package/src/runtime/numbering-prefix.ts +5 -0
- package/src/runtime/paragraph-style-resolver.ts +194 -0
- package/src/runtime/render/block-fragment-projection.ts +35 -0
- package/src/runtime/render/decoration-resolver.ts +189 -0
- package/src/runtime/render/index.ts +57 -0
- package/src/runtime/render/pending-op-delta-reader.ts +129 -0
- package/src/runtime/render/render-frame-types.ts +317 -0
- package/src/runtime/render/render-kernel.ts +759 -0
- package/src/runtime/resolved-numbering-geometry.ts +9 -1
- package/src/runtime/surface-projection.ts +129 -9
- package/src/runtime/table-schema.ts +11 -0
- package/src/runtime/view-state.ts +67 -0
- package/src/runtime/workflow-markup.ts +1 -5
- package/src/runtime/workflow-rail-segments.ts +280 -0
- package/src/ui/WordReviewEditor.tsx +368 -19
- package/src/ui/editor-command-bag.ts +4 -0
- package/src/ui/editor-runtime-boundary.ts +16 -0
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +9 -1
- package/src/ui/headless/chrome-registry.ts +310 -15
- package/src/ui/headless/scoped-chrome-policy.ts +49 -1
- package/src/ui/headless/selection-tool-types.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
- package/src/ui-tailwind/chrome/review-queue-bar.tsx +2 -14
- package/src/ui-tailwind/chrome/role-action-sets.ts +80 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +160 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +68 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-structure.tsx +11 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-border-picker.tsx +245 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +356 -140
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +284 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +94 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +16 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +96 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +4 -0
- package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +11 -0
- package/src/ui-tailwind/editor-surface/page-slice-util.ts +15 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +40 -4
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +389 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +40 -2
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +144 -62
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +179 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +559 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +224 -75
- package/src/ui-tailwind/editor-surface/tw-table-bands.css +61 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +19 -0
- package/src/ui-tailwind/index.ts +29 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
- package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
- package/src/ui-tailwind/theme/editor-theme.css +498 -163
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +680 -0
- package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +104 -2
- package/src/ui-tailwind/tw-review-workspace.tsx +234 -21
- package/src/runtime/collab-review-sync.ts +0 -254
- package/src/ui-tailwind/editor-surface/pm-collab-plugins.ts +0 -40
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from "../document-layout.ts";
|
|
37
37
|
import { findNoteReferencePosition } from "../view-state.ts";
|
|
38
38
|
import { createEditorSurfaceSnapshot } from "../surface-projection.ts";
|
|
39
|
+
import { buildHeadingOutline } from "../document-navigation.ts";
|
|
39
40
|
import {
|
|
40
41
|
analyzeInvalidation,
|
|
41
42
|
computeFieldDirtiness,
|
|
@@ -44,6 +45,8 @@ import {
|
|
|
44
45
|
import {
|
|
45
46
|
buildPageStack,
|
|
46
47
|
buildPageStackFrom,
|
|
48
|
+
buildPageStackFromWithSplits,
|
|
49
|
+
buildPageStackWithSplits,
|
|
47
50
|
type LayoutInvalidationReason,
|
|
48
51
|
} from "./paginated-layout-engine.ts";
|
|
49
52
|
import {
|
|
@@ -53,6 +56,7 @@ import {
|
|
|
53
56
|
type RuntimePageGraph,
|
|
54
57
|
type RuntimePageNode,
|
|
55
58
|
} from "./page-graph.ts";
|
|
59
|
+
import { projectSurfaceBlocksToPageFragments } from "./project-block-fragments.ts";
|
|
56
60
|
import {
|
|
57
61
|
resolvePageStories,
|
|
58
62
|
resolveTotalPageCount,
|
|
@@ -193,6 +197,14 @@ function recordFullRebuildReason(reasonKind: string): void {
|
|
|
193
197
|
export interface CreateLayoutEngineOptions {
|
|
194
198
|
/** Optional measurement provider. Defaults to empirical. */
|
|
195
199
|
measurementProvider?: LayoutMeasurementProvider;
|
|
200
|
+
/**
|
|
201
|
+
* When true and a browser-like `document` global is available, the engine
|
|
202
|
+
* dynamically imports the Canvas2D measurement backend and swaps to it at
|
|
203
|
+
* init time, emitting `measurement_backend_ready`. SSR stays on the
|
|
204
|
+
* empirical backend. Callers that want to stay on empirical (for
|
|
205
|
+
* determinism or tests) pass `false`. Default: true.
|
|
206
|
+
*/
|
|
207
|
+
autoUpgradeToCanvasBackend?: boolean;
|
|
196
208
|
}
|
|
197
209
|
|
|
198
210
|
export function createLayoutEngine(
|
|
@@ -200,6 +212,7 @@ export function createLayoutEngine(
|
|
|
200
212
|
): LayoutEngineInstance {
|
|
201
213
|
let measurementProvider: LayoutMeasurementProvider =
|
|
202
214
|
options.measurementProvider ?? createEmpiricalMeasurementProvider();
|
|
215
|
+
const autoUpgradeToCanvas = options.autoUpgradeToCanvasBackend !== false;
|
|
203
216
|
const dirtyFieldFamilies = new Set<string>();
|
|
204
217
|
const listeners = new Set<(event: LayoutEngineEvent) => void>();
|
|
205
218
|
let cachedKey: CacheKey | null = null;
|
|
@@ -242,9 +255,25 @@ export function createLayoutEngine(
|
|
|
242
255
|
MAIN_STORY_TARGET,
|
|
243
256
|
);
|
|
244
257
|
const sections = buildResolvedSections(document);
|
|
245
|
-
const
|
|
258
|
+
const pageStack = buildPageStackWithSplits(
|
|
259
|
+
document,
|
|
260
|
+
sections,
|
|
261
|
+
mainSurface,
|
|
262
|
+
measurementProvider,
|
|
263
|
+
);
|
|
264
|
+
const pages = pageStack.pages;
|
|
246
265
|
const stories = resolvePageStories(pages);
|
|
247
|
-
const
|
|
266
|
+
const fragmentsByPageIndex = projectSurfaceBlocksToPageFragments(
|
|
267
|
+
mainSurface,
|
|
268
|
+
pages,
|
|
269
|
+
pageStack.splits,
|
|
270
|
+
);
|
|
271
|
+
const graph = buildPageGraph({
|
|
272
|
+
pages,
|
|
273
|
+
sections,
|
|
274
|
+
stories,
|
|
275
|
+
fragmentsByPageIndex,
|
|
276
|
+
});
|
|
248
277
|
|
|
249
278
|
// Field dirtiness diff from previous graph
|
|
250
279
|
const dirtyFamilies = computeFieldDirtiness(cachedGraph, graph);
|
|
@@ -309,10 +338,17 @@ export function createLayoutEngine(
|
|
|
309
338
|
const sections = buildResolvedSections(document);
|
|
310
339
|
|
|
311
340
|
const dirtyPage = priorGraph.pages[firstDirty]!;
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
341
|
+
const freshResult = buildPageStackFromWithSplits(
|
|
342
|
+
document,
|
|
343
|
+
sections,
|
|
344
|
+
mainSurface,
|
|
345
|
+
{
|
|
346
|
+
startPageIndex: firstDirty,
|
|
347
|
+
startOffset: dirtyPage.startOffset,
|
|
348
|
+
},
|
|
349
|
+
measurementProvider,
|
|
350
|
+
);
|
|
351
|
+
const freshSnapshots = freshResult.pages;
|
|
316
352
|
|
|
317
353
|
// Convert fresh DocumentPageSnapshots into RuntimePageNodes via the
|
|
318
354
|
// standard buildPageGraph pipeline — this keeps region, story, and
|
|
@@ -324,10 +360,18 @@ export function createLayoutEngine(
|
|
|
324
360
|
return null;
|
|
325
361
|
}
|
|
326
362
|
const freshStories = resolvePageStories(freshSnapshots);
|
|
363
|
+
// Project fragments for the fresh tail pages, threading paragraph
|
|
364
|
+
// line-range splits produced by intra-paragraph pagination.
|
|
365
|
+
const freshFragmentsByPageIndex = projectSurfaceBlocksToPageFragments(
|
|
366
|
+
mainSurface,
|
|
367
|
+
freshSnapshots,
|
|
368
|
+
freshResult.splits,
|
|
369
|
+
);
|
|
327
370
|
const freshGraph = buildPageGraph({
|
|
328
371
|
pages: freshSnapshots,
|
|
329
372
|
sections,
|
|
330
373
|
stories: freshStories,
|
|
374
|
+
fragmentsByPageIndex: freshFragmentsByPageIndex,
|
|
331
375
|
});
|
|
332
376
|
const freshNodes = freshGraph.pages;
|
|
333
377
|
|
|
@@ -427,6 +471,46 @@ export function createLayoutEngine(
|
|
|
427
471
|
return cachedFormatting!;
|
|
428
472
|
}
|
|
429
473
|
|
|
474
|
+
// -----------------------------------------------------------------------
|
|
475
|
+
// Auto-upgrade to the Canvas2D measurement backend in browsers. Dynamic
|
|
476
|
+
// import keeps SSR bundles lean. We only attempt the upgrade when the
|
|
477
|
+
// caller didn't provide their own provider and `document` is available.
|
|
478
|
+
// -----------------------------------------------------------------------
|
|
479
|
+
if (
|
|
480
|
+
autoUpgradeToCanvas &&
|
|
481
|
+
options.measurementProvider === undefined &&
|
|
482
|
+
typeof document !== "undefined" &&
|
|
483
|
+
typeof HTMLCanvasElement !== "undefined"
|
|
484
|
+
) {
|
|
485
|
+
// Swallow errors silently — staying on empirical is correct behavior
|
|
486
|
+
// when the upgrade fails. Perf probe increments the fallback counter
|
|
487
|
+
// through the emitted event (listeners observe fidelity).
|
|
488
|
+
const readCachedRevision = (): number => cachedGraph?.revision ?? 0;
|
|
489
|
+
void (async () => {
|
|
490
|
+
try {
|
|
491
|
+
const mod = await import("./measurement-backend-canvas.ts");
|
|
492
|
+
const canvasProvider = mod.createCanvasBackend();
|
|
493
|
+
measurementProvider = canvasProvider;
|
|
494
|
+
// Invalidate the cached graph/formatting/mapper so the next read
|
|
495
|
+
// recomputes with canvas-measured font metrics. Without this,
|
|
496
|
+
// the first render after the async import still uses empirical
|
|
497
|
+
// numbers and the chrome shifts by a few pixels on the next
|
|
498
|
+
// real invalidation.
|
|
499
|
+
cachedKey = null;
|
|
500
|
+
cachedGraph = null;
|
|
501
|
+
cachedFormatting = null;
|
|
502
|
+
cachedMapper = null;
|
|
503
|
+
emit({
|
|
504
|
+
kind: "measurement_backend_ready",
|
|
505
|
+
revision: readCachedRevision(),
|
|
506
|
+
fidelity: canvasProvider.fidelity,
|
|
507
|
+
});
|
|
508
|
+
} catch {
|
|
509
|
+
// Stay on empirical. No-op.
|
|
510
|
+
}
|
|
511
|
+
})();
|
|
512
|
+
}
|
|
513
|
+
|
|
430
514
|
return {
|
|
431
515
|
get measurementFidelity() {
|
|
432
516
|
return measurementProvider.fidelity;
|
|
@@ -543,13 +627,19 @@ function buildNavigationFromGraph(
|
|
|
543
627
|
): DocumentNavigationSnapshot {
|
|
544
628
|
const pages = deriveDocumentPageSnapshots(graph);
|
|
545
629
|
const sections = graph.sections;
|
|
630
|
+
const mainSurface = createEditorSurfaceSnapshot(
|
|
631
|
+
document,
|
|
632
|
+
createSelectionSnapshot(0, 0),
|
|
633
|
+
MAIN_STORY_TARGET,
|
|
634
|
+
);
|
|
635
|
+
const headings = buildHeadingOutline(document, mainSurface, sections, pages);
|
|
546
636
|
|
|
547
637
|
if (activeStory.kind === "main") {
|
|
548
638
|
const activePageIndex = deriveActivePageIndex(graph, selectionHead);
|
|
549
639
|
return {
|
|
550
640
|
pageCount: pages.length,
|
|
551
641
|
pages,
|
|
552
|
-
headings
|
|
642
|
+
headings,
|
|
553
643
|
activePageIndex,
|
|
554
644
|
activeSectionIndex: deriveActiveSectionIndex(graph, selectionHead),
|
|
555
645
|
};
|
|
@@ -564,24 +654,19 @@ function buildNavigationFromGraph(
|
|
|
564
654
|
return {
|
|
565
655
|
pageCount: pages.length,
|
|
566
656
|
pages,
|
|
567
|
-
headings
|
|
657
|
+
headings,
|
|
568
658
|
activePageIndex: firstPage >= 0 ? firstPage : 0,
|
|
569
659
|
activeSectionIndex: sectionIndex,
|
|
570
660
|
};
|
|
571
661
|
}
|
|
572
662
|
|
|
573
663
|
if (activeStory.kind === "footnote" || activeStory.kind === "endnote") {
|
|
574
|
-
const mainSurface = createEditorSurfaceSnapshot(
|
|
575
|
-
document,
|
|
576
|
-
createSelectionSnapshot(0, 0),
|
|
577
|
-
MAIN_STORY_TARGET,
|
|
578
|
-
);
|
|
579
664
|
const referencePosition = findNoteReferencePosition(mainSurface, activeStory);
|
|
580
665
|
const activePageIndex = deriveActivePageIndex(graph, referencePosition);
|
|
581
666
|
return {
|
|
582
667
|
pageCount: pages.length,
|
|
583
668
|
pages,
|
|
584
|
-
headings
|
|
669
|
+
headings,
|
|
585
670
|
activePageIndex,
|
|
586
671
|
activeSectionIndex:
|
|
587
672
|
graph.pages[activePageIndex]?.sectionIndex ??
|
|
@@ -592,20 +677,12 @@ function buildNavigationFromGraph(
|
|
|
592
677
|
return {
|
|
593
678
|
pageCount: pages.length,
|
|
594
679
|
pages,
|
|
595
|
-
headings
|
|
680
|
+
headings,
|
|
596
681
|
activePageIndex: 0,
|
|
597
682
|
activeSectionIndex: 0,
|
|
598
683
|
};
|
|
599
684
|
}
|
|
600
685
|
|
|
601
|
-
function buildHeadings(graph: RuntimePageGraph) {
|
|
602
|
-
// Headings are derived elsewhere (createDocumentNavigationSnapshot pipes
|
|
603
|
-
// them from its own helper). For engine-native reads we return an empty
|
|
604
|
-
// list; the existing navigation snapshot keeps its headings pipeline.
|
|
605
|
-
void graph;
|
|
606
|
-
return [];
|
|
607
|
-
}
|
|
608
|
-
|
|
609
686
|
// ---------------------------------------------------------------------------
|
|
610
687
|
// Convenience: find the active page node directly
|
|
611
688
|
// ---------------------------------------------------------------------------
|
|
@@ -93,12 +93,21 @@ export function analyzeInvalidation(
|
|
|
93
93
|
return analyzeSectionChange(reason, graph);
|
|
94
94
|
|
|
95
95
|
case "numbering-change":
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
if (!reason.numberingInstanceId) {
|
|
97
|
+
return {
|
|
98
|
+
scope: "full",
|
|
99
|
+
requiresFullRecompute: true,
|
|
100
|
+
dirtyFieldFamilies: [],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
99
103
|
return {
|
|
100
|
-
scope: "
|
|
101
|
-
requiresFullRecompute:
|
|
104
|
+
scope: "bounded",
|
|
105
|
+
requiresFullRecompute: false,
|
|
106
|
+
dirtyPageRange: {
|
|
107
|
+
firstPageIndex: 0,
|
|
108
|
+
lastPageIndex: Math.max(0, graph.pages.length - 1),
|
|
109
|
+
},
|
|
110
|
+
dirtySectionRange: null,
|
|
102
111
|
dirtyFieldFamilies: [],
|
|
103
112
|
};
|
|
104
113
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Margin preset catalog — the five named Word margin presets (Normal,
|
|
3
|
+
* Narrow, Moderate, Wide, Mirrored) plus Custom.
|
|
4
|
+
*
|
|
5
|
+
* Values mirror Microsoft Word's defaults in twips so a "Narrow" preset
|
|
6
|
+
* round-trips to `w:top = 720 / w:bottom = 720 / w:left = 720 / w:right = 720`
|
|
7
|
+
* on export.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Public types
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export type MarginPresetId =
|
|
15
|
+
| "normal"
|
|
16
|
+
| "narrow"
|
|
17
|
+
| "moderate"
|
|
18
|
+
| "wide"
|
|
19
|
+
| "mirrored"
|
|
20
|
+
| "custom";
|
|
21
|
+
|
|
22
|
+
export interface MarginPresetDefinition {
|
|
23
|
+
id: MarginPresetId;
|
|
24
|
+
label: string;
|
|
25
|
+
topTwips: number;
|
|
26
|
+
bottomTwips: number;
|
|
27
|
+
leftTwips: number;
|
|
28
|
+
rightTwips: number;
|
|
29
|
+
gutterTwips: number;
|
|
30
|
+
mirrored: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ActiveMarginPreset {
|
|
34
|
+
sectionIndex: number;
|
|
35
|
+
preset: MarginPresetDefinition;
|
|
36
|
+
matchesCatalog: boolean;
|
|
37
|
+
customTwips?: {
|
|
38
|
+
top: number;
|
|
39
|
+
bottom: number;
|
|
40
|
+
left: number;
|
|
41
|
+
right: number;
|
|
42
|
+
gutter: number;
|
|
43
|
+
mirrored: boolean;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Catalog
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
export const MARGIN_PRESET_CATALOG: readonly MarginPresetDefinition[] = Object.freeze([
|
|
52
|
+
{
|
|
53
|
+
id: "normal",
|
|
54
|
+
label: "Normal",
|
|
55
|
+
topTwips: 1440,
|
|
56
|
+
bottomTwips: 1440,
|
|
57
|
+
leftTwips: 1440,
|
|
58
|
+
rightTwips: 1440,
|
|
59
|
+
gutterTwips: 0,
|
|
60
|
+
mirrored: false,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "narrow",
|
|
64
|
+
label: "Narrow",
|
|
65
|
+
topTwips: 720,
|
|
66
|
+
bottomTwips: 720,
|
|
67
|
+
leftTwips: 720,
|
|
68
|
+
rightTwips: 720,
|
|
69
|
+
gutterTwips: 0,
|
|
70
|
+
mirrored: false,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "moderate",
|
|
74
|
+
label: "Moderate",
|
|
75
|
+
topTwips: 1440,
|
|
76
|
+
bottomTwips: 1440,
|
|
77
|
+
leftTwips: 1080,
|
|
78
|
+
rightTwips: 1080,
|
|
79
|
+
gutterTwips: 0,
|
|
80
|
+
mirrored: false,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "wide",
|
|
84
|
+
label: "Wide",
|
|
85
|
+
topTwips: 1440,
|
|
86
|
+
bottomTwips: 1440,
|
|
87
|
+
leftTwips: 2880,
|
|
88
|
+
rightTwips: 2880,
|
|
89
|
+
gutterTwips: 0,
|
|
90
|
+
mirrored: false,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "mirrored",
|
|
94
|
+
label: "Mirrored",
|
|
95
|
+
topTwips: 1440,
|
|
96
|
+
bottomTwips: 1440,
|
|
97
|
+
leftTwips: 1800,
|
|
98
|
+
rightTwips: 1440,
|
|
99
|
+
gutterTwips: 0,
|
|
100
|
+
mirrored: true,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "custom",
|
|
104
|
+
label: "Custom",
|
|
105
|
+
topTwips: 0,
|
|
106
|
+
bottomTwips: 0,
|
|
107
|
+
leftTwips: 0,
|
|
108
|
+
rightTwips: 0,
|
|
109
|
+
gutterTwips: 0,
|
|
110
|
+
mirrored: false,
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const MATCH_TOLERANCE_TWIPS = 1;
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Matching
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
export interface MatchMarginPresetInput {
|
|
121
|
+
sectionIndex: number;
|
|
122
|
+
topTwips: number;
|
|
123
|
+
bottomTwips: number;
|
|
124
|
+
leftTwips: number;
|
|
125
|
+
rightTwips: number;
|
|
126
|
+
gutterTwips: number;
|
|
127
|
+
mirrored: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function matchMarginPreset(
|
|
131
|
+
input: MatchMarginPresetInput,
|
|
132
|
+
): ActiveMarginPreset {
|
|
133
|
+
const { sectionIndex, topTwips, bottomTwips, leftTwips, rightTwips, gutterTwips, mirrored } = input;
|
|
134
|
+
|
|
135
|
+
for (const preset of MARGIN_PRESET_CATALOG) {
|
|
136
|
+
if (preset.id === "custom") continue;
|
|
137
|
+
if (preset.mirrored !== mirrored) continue;
|
|
138
|
+
|
|
139
|
+
if (
|
|
140
|
+
near(preset.topTwips, topTwips) &&
|
|
141
|
+
near(preset.bottomTwips, bottomTwips) &&
|
|
142
|
+
near(preset.leftTwips, leftTwips) &&
|
|
143
|
+
near(preset.rightTwips, rightTwips) &&
|
|
144
|
+
near(preset.gutterTwips, gutterTwips)
|
|
145
|
+
) {
|
|
146
|
+
return {
|
|
147
|
+
sectionIndex,
|
|
148
|
+
preset,
|
|
149
|
+
matchesCatalog: true,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const custom = MARGIN_PRESET_CATALOG.find((p) => p.id === "custom")!;
|
|
155
|
+
return {
|
|
156
|
+
sectionIndex,
|
|
157
|
+
preset: custom,
|
|
158
|
+
matchesCatalog: false,
|
|
159
|
+
customTwips: {
|
|
160
|
+
top: topTwips,
|
|
161
|
+
bottom: bottomTwips,
|
|
162
|
+
left: leftTwips,
|
|
163
|
+
right: rightTwips,
|
|
164
|
+
gutter: gutterTwips,
|
|
165
|
+
mirrored,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function near(a: number, b: number): boolean {
|
|
171
|
+
return Math.abs(a - b) <= MATCH_TOLERANCE_TWIPS;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function getMarginPresetById(id: string): MarginPresetDefinition {
|
|
175
|
+
const found = MARGIN_PRESET_CATALOG.find((p) => p.id === id);
|
|
176
|
+
if (found) return found;
|
|
177
|
+
return MARGIN_PRESET_CATALOG.find((p) => p.id === "custom")!;
|
|
178
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-format catalog — named page sizes that back the section page-size
|
|
3
|
+
* picker and the real-dimension page frame.
|
|
4
|
+
*
|
|
5
|
+
* The canonical storage of page geometry remains
|
|
6
|
+
* `SectionProperties.pageSize` in twips. This catalog adds a layer of
|
|
7
|
+
* semantic naming on top so the UI can render "A4" or "US Letter" and
|
|
8
|
+
* the render kernel can pick a sensible default per locale.
|
|
9
|
+
*
|
|
10
|
+
* Unit reference (OOXML):
|
|
11
|
+
* - 1 inch = 1440 twips
|
|
12
|
+
* - 1 mm = 56.6929 twips
|
|
13
|
+
*
|
|
14
|
+
* The catalog deliberately ships a single default match tolerance (1 twip)
|
|
15
|
+
* so legacy documents whose page sizes were round-tripped through third-
|
|
16
|
+
* party tools still identify as the named format they were authored at.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Public types
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export type PageFormatId =
|
|
24
|
+
| "letter"
|
|
25
|
+
| "legal"
|
|
26
|
+
| "tabloid"
|
|
27
|
+
| "executive"
|
|
28
|
+
| "a3"
|
|
29
|
+
| "a4"
|
|
30
|
+
| "a5"
|
|
31
|
+
| "b4-iso"
|
|
32
|
+
| "b5-iso"
|
|
33
|
+
| "custom";
|
|
34
|
+
|
|
35
|
+
export type PageFormatRegion = "us" | "iso" | "jp" | "custom";
|
|
36
|
+
|
|
37
|
+
export type PageFormatLocaleDefault = "en-us" | "en-gb" | "eu" | "jp";
|
|
38
|
+
|
|
39
|
+
export interface PageFormatDisplay {
|
|
40
|
+
inches?: string;
|
|
41
|
+
millimeters?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface PageFormatDefinition {
|
|
45
|
+
/** Stable identifier. */
|
|
46
|
+
id: PageFormatId;
|
|
47
|
+
/** Human-facing label used in pickers. */
|
|
48
|
+
label: string;
|
|
49
|
+
/** Locale or region hint. */
|
|
50
|
+
region: PageFormatRegion;
|
|
51
|
+
/** Width in twips at portrait orientation. */
|
|
52
|
+
portraitWidthTwips: number;
|
|
53
|
+
/** Height in twips at portrait orientation. */
|
|
54
|
+
portraitHeightTwips: number;
|
|
55
|
+
/** Default-for-locale hint so consumers can pick a sensible default. */
|
|
56
|
+
localeDefault?: PageFormatLocaleDefault;
|
|
57
|
+
/** Pre-computed inches/millimeters strings for the picker. */
|
|
58
|
+
display: PageFormatDisplay;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ActivePageFormat {
|
|
62
|
+
sectionIndex: number;
|
|
63
|
+
format: PageFormatDefinition;
|
|
64
|
+
orientation: "portrait" | "landscape";
|
|
65
|
+
/** True when the section matches a catalog format within the match tolerance. */
|
|
66
|
+
matchesCatalog: boolean;
|
|
67
|
+
/** When matchesCatalog is false, the raw twips (custom format). */
|
|
68
|
+
customTwips?: { width: number; height: number };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Catalog
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Pre-defined named page formats. Ordering intentionally puts the most
|
|
77
|
+
* common sizes (Letter, A4) first because UI dropdowns render in list order.
|
|
78
|
+
*/
|
|
79
|
+
export const PAGE_FORMAT_CATALOG: readonly PageFormatDefinition[] = Object.freeze([
|
|
80
|
+
{
|
|
81
|
+
id: "letter",
|
|
82
|
+
label: "US Letter",
|
|
83
|
+
region: "us",
|
|
84
|
+
portraitWidthTwips: 12240,
|
|
85
|
+
portraitHeightTwips: 15840,
|
|
86
|
+
localeDefault: "en-us",
|
|
87
|
+
display: { inches: "8.5 × 11 in", millimeters: "215.9 × 279.4 mm" },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "a4",
|
|
91
|
+
label: "A4",
|
|
92
|
+
region: "iso",
|
|
93
|
+
portraitWidthTwips: 11906,
|
|
94
|
+
portraitHeightTwips: 16838,
|
|
95
|
+
localeDefault: "eu",
|
|
96
|
+
display: { inches: "8.27 × 11.69 in", millimeters: "210 × 297 mm" },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "legal",
|
|
100
|
+
label: "US Legal",
|
|
101
|
+
region: "us",
|
|
102
|
+
portraitWidthTwips: 12240,
|
|
103
|
+
portraitHeightTwips: 20160,
|
|
104
|
+
display: { inches: "8.5 × 14 in", millimeters: "215.9 × 355.6 mm" },
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "tabloid",
|
|
108
|
+
label: "Tabloid / Ledger",
|
|
109
|
+
region: "us",
|
|
110
|
+
portraitWidthTwips: 15840,
|
|
111
|
+
portraitHeightTwips: 24480,
|
|
112
|
+
display: { inches: "11 × 17 in", millimeters: "279.4 × 431.8 mm" },
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "executive",
|
|
116
|
+
label: "Executive",
|
|
117
|
+
region: "us",
|
|
118
|
+
portraitWidthTwips: 10440,
|
|
119
|
+
portraitHeightTwips: 15120,
|
|
120
|
+
display: { inches: "7.25 × 10.5 in", millimeters: "184.1 × 266.7 mm" },
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "a3",
|
|
124
|
+
label: "A3",
|
|
125
|
+
region: "iso",
|
|
126
|
+
portraitWidthTwips: 16838,
|
|
127
|
+
portraitHeightTwips: 23811,
|
|
128
|
+
display: { inches: "11.69 × 16.54 in", millimeters: "297 × 420 mm" },
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "a5",
|
|
132
|
+
label: "A5",
|
|
133
|
+
region: "iso",
|
|
134
|
+
portraitWidthTwips: 8391,
|
|
135
|
+
portraitHeightTwips: 11906,
|
|
136
|
+
display: { inches: "5.83 × 8.27 in", millimeters: "148 × 210 mm" },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "b4-iso",
|
|
140
|
+
label: "B4 (ISO)",
|
|
141
|
+
region: "iso",
|
|
142
|
+
portraitWidthTwips: 14173,
|
|
143
|
+
portraitHeightTwips: 20016,
|
|
144
|
+
display: { inches: "9.84 × 13.90 in", millimeters: "250 × 353 mm" },
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "b5-iso",
|
|
148
|
+
label: "B5 (ISO)",
|
|
149
|
+
region: "iso",
|
|
150
|
+
portraitWidthTwips: 9977,
|
|
151
|
+
portraitHeightTwips: 14173,
|
|
152
|
+
display: { inches: "6.93 × 9.84 in", millimeters: "176 × 250 mm" },
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "custom",
|
|
156
|
+
label: "Custom",
|
|
157
|
+
region: "custom",
|
|
158
|
+
portraitWidthTwips: 0,
|
|
159
|
+
portraitHeightTwips: 0,
|
|
160
|
+
display: {},
|
|
161
|
+
},
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Match tolerance in twips. Historic round-trips through third-party tools
|
|
166
|
+
* sometimes drift by sub-twip amounts; a 1-twip window catches those while
|
|
167
|
+
* keeping intentionally custom sizes genuinely custom.
|
|
168
|
+
*/
|
|
169
|
+
const MATCH_TOLERANCE_TWIPS = 1;
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Matching
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
export interface MatchPageFormatInput {
|
|
176
|
+
sectionIndex: number;
|
|
177
|
+
widthTwips: number;
|
|
178
|
+
heightTwips: number;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Match a section's page dimensions against the catalog.
|
|
183
|
+
*
|
|
184
|
+
* The matcher tolerates either orientation — if the supplied (width, height)
|
|
185
|
+
* match a catalog entry rotated by 90°, the active format is reported as
|
|
186
|
+
* `landscape`. Custom sizes fall through to the `custom` entry.
|
|
187
|
+
*/
|
|
188
|
+
export function matchPageFormat(input: MatchPageFormatInput): ActivePageFormat {
|
|
189
|
+
const { sectionIndex, widthTwips, heightTwips } = input;
|
|
190
|
+
|
|
191
|
+
for (const format of PAGE_FORMAT_CATALOG) {
|
|
192
|
+
if (format.id === "custom") continue;
|
|
193
|
+
|
|
194
|
+
const portraitMatch =
|
|
195
|
+
near(widthTwips, format.portraitWidthTwips) &&
|
|
196
|
+
near(heightTwips, format.portraitHeightTwips);
|
|
197
|
+
const landscapeMatch =
|
|
198
|
+
near(widthTwips, format.portraitHeightTwips) &&
|
|
199
|
+
near(heightTwips, format.portraitWidthTwips);
|
|
200
|
+
|
|
201
|
+
if (portraitMatch || landscapeMatch) {
|
|
202
|
+
return {
|
|
203
|
+
sectionIndex,
|
|
204
|
+
format,
|
|
205
|
+
orientation: portraitMatch ? "portrait" : "landscape",
|
|
206
|
+
matchesCatalog: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const custom = PAGE_FORMAT_CATALOG.find((f) => f.id === "custom")!;
|
|
212
|
+
return {
|
|
213
|
+
sectionIndex,
|
|
214
|
+
format: custom,
|
|
215
|
+
orientation: widthTwips > heightTwips ? "landscape" : "portrait",
|
|
216
|
+
matchesCatalog: false,
|
|
217
|
+
customTwips: { width: widthTwips, height: heightTwips },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function near(a: number, b: number): boolean {
|
|
222
|
+
return Math.abs(a - b) <= MATCH_TOLERANCE_TWIPS;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Look up a format by id. Returns the `custom` entry for unknown ids so
|
|
227
|
+
* callers can chain `.format` reads without null-checking.
|
|
228
|
+
*/
|
|
229
|
+
export function getPageFormatById(id: string): PageFormatDefinition {
|
|
230
|
+
const found = PAGE_FORMAT_CATALOG.find((f) => f.id === id);
|
|
231
|
+
if (found) return found;
|
|
232
|
+
return PAGE_FORMAT_CATALOG.find((f) => f.id === "custom")!;
|
|
233
|
+
}
|