@beyondwork/docx-react-component 1.0.37 → 1.0.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api/public-types.ts +319 -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 +815 -55
- package/src/io/export/serialize-main-document.ts +2 -11
- package/src/io/export/serialize-numbering.ts +1 -2
- 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-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-tables.ts +249 -0
- package/src/model/canonical-document.ts +34 -0
- package/src/runtime/document-layout.ts +4 -2
- package/src/runtime/document-navigation.ts +1 -1
- package/src/runtime/document-runtime.ts +114 -0
- package/src/runtime/layout/default-page-format.ts +96 -0
- package/src/runtime/layout/index.ts +45 -0
- package/src/runtime/layout/inert-layout-facet.ts +14 -0
- package/src/runtime/layout/layout-engine-instance.ts +33 -23
- 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 +19 -0
- package/src/runtime/layout/paginated-layout-engine.ts +142 -9
- package/src/runtime/layout/project-block-fragments.ts +91 -0
- package/src/runtime/layout/public-facet.ts +709 -16
- package/src/runtime/layout/table-render-plan.ts +229 -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 +755 -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 +84 -15
- package/src/ui/editor-shell-view.tsx +6 -0
- package/src/ui/headless/chrome-registry.ts +280 -14
- package/src/ui/headless/scoped-chrome-policy.ts +20 -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/role-action-sets.ts +74 -0
- package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
- package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
- package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
- package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -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/perf-probe.ts +7 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +12 -1
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +22 -12
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +3 -0
- package/src/ui-tailwind/index.ts +33 -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 +559 -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 +69 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +136 -1
|
@@ -124,11 +124,16 @@ import {
|
|
|
124
124
|
findPageForOffset,
|
|
125
125
|
} from "./document-navigation.ts";
|
|
126
126
|
import {
|
|
127
|
+
createDocxFontLoader,
|
|
127
128
|
createLayoutEngine,
|
|
128
129
|
createLayoutFacet,
|
|
130
|
+
createMeasurementProvider,
|
|
131
|
+
type DocxFontLoader,
|
|
129
132
|
type LayoutEngineInstance,
|
|
133
|
+
type LayoutMeasurementProvider,
|
|
130
134
|
type WordReviewEditorLayoutFacet,
|
|
131
135
|
} from "./layout/index.ts";
|
|
136
|
+
import { createRenderKernel, type RenderKernel } from "./render/index.ts";
|
|
132
137
|
import {
|
|
133
138
|
createDocumentOutlineSnapshot,
|
|
134
139
|
createDocumentSectionSnapshots,
|
|
@@ -405,7 +410,20 @@ export function createDocumentRuntime(
|
|
|
405
410
|
// The engine caches graph + resolved-formatting + fragment mapper keyed on
|
|
406
411
|
// (content, styles, subParts). It is the single internal source of truth
|
|
407
412
|
// for page composition, story resolution, and layout invalidation.
|
|
413
|
+
//
|
|
414
|
+
// R0 measurement wiring: the engine starts with the sync empirical backend
|
|
415
|
+
// so the runtime is available immediately, then we kick off
|
|
416
|
+
// `createMeasurementProvider({ preference: "auto", fontLoader })` which
|
|
417
|
+
// upgrades to the canvas backend once fonts resolve. `swapMeasurementProvider`
|
|
418
|
+
// emits `measurement_backend_ready` so chrome consumers can re-read metrics.
|
|
408
419
|
const layoutEngine: LayoutEngineInstance = createLayoutEngine();
|
|
420
|
+
const fontLoader: DocxFontLoader = createDocxFontLoader(
|
|
421
|
+
collectFontLoaderInput(state.document),
|
|
422
|
+
);
|
|
423
|
+
void upgradeMeasurementProvider(layoutEngine, fontLoader);
|
|
424
|
+
// `renderKernelRef` is a forward reference so the facet can reach the
|
|
425
|
+
// kernel after it is created below (kernel creation needs the facet).
|
|
426
|
+
let renderKernelRef: RenderKernel | null = null;
|
|
409
427
|
const layoutFacet: WordReviewEditorLayoutFacet = createLayoutFacet({
|
|
410
428
|
engine: layoutEngine,
|
|
411
429
|
getQueryInput: () => ({
|
|
@@ -416,6 +434,27 @@ export function createDocumentRuntime(
|
|
|
416
434
|
zoomLevel: viewState.zoomLevel,
|
|
417
435
|
},
|
|
418
436
|
}),
|
|
437
|
+
renderKernel: () => renderKernelRef,
|
|
438
|
+
getWorkflowRailInput: () => {
|
|
439
|
+
if (!workflowOverlay) return null;
|
|
440
|
+
const activeWorkItemId = workflowOverlay.activeWorkItemId ?? null;
|
|
441
|
+
const activeWorkItem =
|
|
442
|
+
activeWorkItemId !== null
|
|
443
|
+
? workflowOverlay.workItems?.find(
|
|
444
|
+
(item) => item.workItemId === activeWorkItemId,
|
|
445
|
+
)
|
|
446
|
+
: undefined;
|
|
447
|
+
return {
|
|
448
|
+
scopes: workflowOverlay.scopes,
|
|
449
|
+
candidates: workflowOverlay.candidates,
|
|
450
|
+
activeWorkItemScopeIds: activeWorkItem?.scopeIds ?? [],
|
|
451
|
+
activeStory,
|
|
452
|
+
};
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
renderKernelRef = createRenderKernel({
|
|
456
|
+
facet: layoutFacet,
|
|
457
|
+
getActiveStory: () => activeStory,
|
|
419
458
|
});
|
|
420
459
|
let cachedSurface:
|
|
421
460
|
| {
|
|
@@ -2336,6 +2375,14 @@ export function createDocumentRuntime(
|
|
|
2336
2375
|
}
|
|
2337
2376
|
}
|
|
2338
2377
|
|
|
2378
|
+
// Font-loader refresh on subParts identity change — this is the
|
|
2379
|
+
// lightweight proxy for "a change that could affect which fonts the
|
|
2380
|
+
// canvas backend measures against". Typing edits don't rebuild
|
|
2381
|
+
// subParts; style + font + numbering imports do.
|
|
2382
|
+
if (previous.document.subParts !== state.document.subParts) {
|
|
2383
|
+
fontLoader.refresh(collectFontLoaderInput(state.document));
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2339
2386
|
cachedRenderSnapshot = refreshRenderSnapshot();
|
|
2340
2387
|
notify(previous, state, transaction);
|
|
2341
2388
|
}
|
|
@@ -4390,3 +4437,70 @@ function remapProtectionSnapshot(
|
|
|
4390
4437
|
preservedRangeCount: nextRanges.filter((range) => !range.enforced).length,
|
|
4391
4438
|
};
|
|
4392
4439
|
}
|
|
4440
|
+
|
|
4441
|
+
// ---------------------------------------------------------------------------
|
|
4442
|
+
// Measurement provider wiring (R0)
|
|
4443
|
+
// ---------------------------------------------------------------------------
|
|
4444
|
+
|
|
4445
|
+
/**
|
|
4446
|
+
* Build the initial input the `DocxFontLoader` needs: a list of font
|
|
4447
|
+
* families the document actively uses, plus any embedded font payloads the
|
|
4448
|
+
* import pipeline may have extracted.
|
|
4449
|
+
*
|
|
4450
|
+
* Walks the document content tree once per call. Embedded font extraction
|
|
4451
|
+
* is not yet wired into the canonical model; we pass an empty map today and
|
|
4452
|
+
* let the loader register system fonts it finds via
|
|
4453
|
+
* `document.fonts.check(...)`.
|
|
4454
|
+
*/
|
|
4455
|
+
function collectFontLoaderInput(
|
|
4456
|
+
document: CanonicalDocumentEnvelope,
|
|
4457
|
+
): { families: readonly string[] } {
|
|
4458
|
+
try {
|
|
4459
|
+
const families = new Set<string>();
|
|
4460
|
+
const visit = (node: unknown): void => {
|
|
4461
|
+
if (!node || typeof node !== "object") return;
|
|
4462
|
+
const record = node as Record<string, unknown>;
|
|
4463
|
+
const rpr = record["runProperties"] as
|
|
4464
|
+
| Record<string, unknown>
|
|
4465
|
+
| undefined;
|
|
4466
|
+
if (rpr && typeof rpr["fontFamily"] === "string") {
|
|
4467
|
+
families.add(rpr["fontFamily"] as string);
|
|
4468
|
+
}
|
|
4469
|
+
for (const value of Object.values(record)) {
|
|
4470
|
+
if (Array.isArray(value)) value.forEach(visit);
|
|
4471
|
+
else if (value && typeof value === "object") visit(value);
|
|
4472
|
+
}
|
|
4473
|
+
};
|
|
4474
|
+
visit(document.content);
|
|
4475
|
+
if (document.styles) {
|
|
4476
|
+
visit(document.styles);
|
|
4477
|
+
}
|
|
4478
|
+
return { families: Array.from(families) };
|
|
4479
|
+
} catch {
|
|
4480
|
+
return { families: [] };
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
/**
|
|
4485
|
+
* Asynchronously upgrade the engine's measurement backend to canvas once
|
|
4486
|
+
* the platform supports it and fonts have resolved. Errors are swallowed
|
|
4487
|
+
* so a failure in the upgrade path can never break the empirical baseline.
|
|
4488
|
+
*/
|
|
4489
|
+
async function upgradeMeasurementProvider(
|
|
4490
|
+
engine: LayoutEngineInstance,
|
|
4491
|
+
fontLoader: DocxFontLoader,
|
|
4492
|
+
): Promise<void> {
|
|
4493
|
+
try {
|
|
4494
|
+
const provider: LayoutMeasurementProvider = await createMeasurementProvider({
|
|
4495
|
+
preference: "auto",
|
|
4496
|
+
fontLoader,
|
|
4497
|
+
});
|
|
4498
|
+
// If the host is running in SSR or a jsdom test shell, the factory will
|
|
4499
|
+
// fall back to the empirical backend. In that case swapping is a no-op
|
|
4500
|
+
// but still emits `measurement_backend_ready` with `empirical` which is
|
|
4501
|
+
// informational; chrome consumers use the event to refresh metrics.
|
|
4502
|
+
engine.swapMeasurementProvider(provider);
|
|
4503
|
+
} catch {
|
|
4504
|
+
// fall through — the empirical backend remains in place
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale-aware default page format.
|
|
3
|
+
*
|
|
4
|
+
* Word historically defaults to US Letter for `en-US` hosts and A4 everywhere
|
|
5
|
+
* else. This module is the single place that decides which format a
|
|
6
|
+
* newly-created document uses when no section carries an explicit `w:pgSz`.
|
|
7
|
+
*
|
|
8
|
+
* It never overrides an existing section's page size on import — importers
|
|
9
|
+
* always preserve what the source document specified. The default is
|
|
10
|
+
* consulted in two places:
|
|
11
|
+
*
|
|
12
|
+
* 1. `serialize-main-document.ts` when the canonical model carries a
|
|
13
|
+
* section with no `pageSize` (e.g. programmatic document construction).
|
|
14
|
+
* 2. `DocumentRuntime` when rendering a brand-new blank document.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
getPageFormatById,
|
|
19
|
+
type PageFormatDefinition,
|
|
20
|
+
} from "./page-format-catalog.ts";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Locale resolution
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Which BCP-47 language tags should fall back to Letter.
|
|
28
|
+
*
|
|
29
|
+
* Anything else defaults to A4. This intentionally uses a small whitelist
|
|
30
|
+
* rather than a `startsWith("en")` check — `en-GB`, `en-AU`, `en-IN` all
|
|
31
|
+
* expect ISO paper sizes, not US Letter.
|
|
32
|
+
*/
|
|
33
|
+
const LETTER_LOCALES = new Set<string>([
|
|
34
|
+
"en-us",
|
|
35
|
+
"en-ca",
|
|
36
|
+
"fr-ca",
|
|
37
|
+
"es-mx",
|
|
38
|
+
"es-cl",
|
|
39
|
+
"fil-ph",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
export interface ResolveDefaultPageFormatOptions {
|
|
43
|
+
/** Explicit BCP-47 locale (e.g. "en-US", "de-DE"). */
|
|
44
|
+
locale?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Return the default `PageFormatDefinition` for a given locale.
|
|
49
|
+
*
|
|
50
|
+
* When `locale` is omitted the function consults `Intl.DateTimeFormat` to
|
|
51
|
+
* infer the current locale. When `Intl` is unavailable (unusual in modern
|
|
52
|
+
* runtimes) it falls back to A4 as the safer international default.
|
|
53
|
+
*/
|
|
54
|
+
export function resolveDefaultPageFormat(
|
|
55
|
+
options: ResolveDefaultPageFormatOptions = {},
|
|
56
|
+
): PageFormatDefinition {
|
|
57
|
+
const raw = options.locale ?? tryResolveHostLocale();
|
|
58
|
+
if (!raw) {
|
|
59
|
+
return getPageFormatById("a4");
|
|
60
|
+
}
|
|
61
|
+
const normalized = raw.toLowerCase();
|
|
62
|
+
if (LETTER_LOCALES.has(normalized)) {
|
|
63
|
+
return getPageFormatById("letter");
|
|
64
|
+
}
|
|
65
|
+
// Match just the language-region prefix (e.g. "en-us-1234" → "en-us")
|
|
66
|
+
const region = normalized.split("-").slice(0, 2).join("-");
|
|
67
|
+
if (LETTER_LOCALES.has(region)) {
|
|
68
|
+
return getPageFormatById("letter");
|
|
69
|
+
}
|
|
70
|
+
return getPageFormatById("a4");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function tryResolveHostLocale(): string | undefined {
|
|
74
|
+
try {
|
|
75
|
+
if (typeof Intl === "undefined" || typeof Intl.DateTimeFormat !== "function") {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return new Intl.DateTimeFormat().resolvedOptions().locale;
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Return the default `w:pgSz` payload (width/height in twips) for a given
|
|
86
|
+
* locale. Convenience wrapper used by the export pipeline.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveDefaultPageSizeTwips(
|
|
89
|
+
options: ResolveDefaultPageFormatOptions = {},
|
|
90
|
+
): { widthTwips: number; heightTwips: number } {
|
|
91
|
+
const format = resolveDefaultPageFormat(options);
|
|
92
|
+
return {
|
|
93
|
+
widthTwips: format.portraitWidthTwips,
|
|
94
|
+
heightTwips: format.portraitHeightTwips,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -184,5 +184,50 @@ export {
|
|
|
184
184
|
type PublicFieldDirtinessReport,
|
|
185
185
|
type LayoutFacetEvent,
|
|
186
186
|
type LayoutFacetInvalidationReason,
|
|
187
|
+
type RenderZoomSummary,
|
|
187
188
|
type CreateLayoutFacetInput,
|
|
189
|
+
type PageFormatDefinition,
|
|
190
|
+
type ActivePageFormat,
|
|
191
|
+
type MarginPresetDefinition,
|
|
192
|
+
type ActiveMarginPreset,
|
|
188
193
|
} from "./public-facet.ts";
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Page-format catalog + margin preset catalog + locale defaults (R0.5)
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
export {
|
|
200
|
+
PAGE_FORMAT_CATALOG,
|
|
201
|
+
matchPageFormat,
|
|
202
|
+
getPageFormatById,
|
|
203
|
+
type PageFormatId,
|
|
204
|
+
type PageFormatRegion,
|
|
205
|
+
type PageFormatLocaleDefault,
|
|
206
|
+
type PageFormatDisplay,
|
|
207
|
+
type MatchPageFormatInput,
|
|
208
|
+
} from "./page-format-catalog.ts";
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
MARGIN_PRESET_CATALOG,
|
|
212
|
+
matchMarginPreset,
|
|
213
|
+
getMarginPresetById,
|
|
214
|
+
type MarginPresetId,
|
|
215
|
+
type MatchMarginPresetInput,
|
|
216
|
+
} from "./margin-preset-catalog.ts";
|
|
217
|
+
|
|
218
|
+
export {
|
|
219
|
+
resolveDefaultPageFormat,
|
|
220
|
+
resolveDefaultPageSizeTwips,
|
|
221
|
+
type ResolveDefaultPageFormatOptions,
|
|
222
|
+
} from "./default-page-format.ts";
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// Workflow rail segments (R3a)
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
|
|
228
|
+
export {
|
|
229
|
+
collectScopeRailSegments,
|
|
230
|
+
type CollectScopeRailSegmentsInput,
|
|
231
|
+
type ScopeRailPosture,
|
|
232
|
+
type ScopeRailSegment,
|
|
233
|
+
} from "../workflow-rail-segments.ts";
|
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
PublicMeasurementFidelity,
|
|
13
13
|
WordReviewEditorLayoutFacet,
|
|
14
14
|
} from "./public-facet.ts";
|
|
15
|
+
import { MARGIN_PRESET_CATALOG } from "./margin-preset-catalog.ts";
|
|
16
|
+
import { PAGE_FORMAT_CATALOG } from "./page-format-catalog.ts";
|
|
15
17
|
|
|
16
18
|
export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
17
19
|
const emptyReport: PublicFieldDirtinessReport = {
|
|
@@ -32,12 +34,24 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
|
|
|
32
34
|
getActiveStoriesOnPage: () => null,
|
|
33
35
|
getDisplayPageNumber: () => null,
|
|
34
36
|
getLineBoxes: () => [],
|
|
37
|
+
getLineBoxesForRegion: () => [],
|
|
35
38
|
getFragmentsForPage: () => [],
|
|
39
|
+
getPageFormatCatalog: () => PAGE_FORMAT_CATALOG,
|
|
40
|
+
getActivePageFormat: () => null,
|
|
41
|
+
getMarginPresetCatalog: () => MARGIN_PRESET_CATALOG,
|
|
42
|
+
getActiveMarginPreset: () => null,
|
|
43
|
+
getRenderFrame: () => null,
|
|
44
|
+
getRenderZoom: () => null,
|
|
45
|
+
hitTest: () => null,
|
|
46
|
+
getAnchorRects: () => [],
|
|
47
|
+
getScopeRailSegments: () => [],
|
|
48
|
+
getAllScopeRailSegments: () => [],
|
|
36
49
|
getResolvedFormatting: () => null,
|
|
37
50
|
getResolvedRunFormatting: () => null,
|
|
38
51
|
getMeasurement: () => null,
|
|
39
52
|
getMeasurementFidelity: () => fidelity,
|
|
40
53
|
whenMeasurementReady: () => Promise.resolve(),
|
|
54
|
+
getTableRenderPlan: () => null,
|
|
41
55
|
getDirtyFieldFamilies: () => [],
|
|
42
56
|
getFieldDirtinessReport: () => emptyReport,
|
|
43
57
|
subscribe: (_listener: (event: LayoutFacetEvent) => void) => () => undefined,
|
|
@@ -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,
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
type RuntimePageGraph,
|
|
54
55
|
type RuntimePageNode,
|
|
55
56
|
} from "./page-graph.ts";
|
|
57
|
+
import { projectSurfaceBlocksToPageFragments } from "./project-block-fragments.ts";
|
|
56
58
|
import {
|
|
57
59
|
resolvePageStories,
|
|
58
60
|
resolveTotalPageCount,
|
|
@@ -242,9 +244,18 @@ export function createLayoutEngine(
|
|
|
242
244
|
MAIN_STORY_TARGET,
|
|
243
245
|
);
|
|
244
246
|
const sections = buildResolvedSections(document);
|
|
245
|
-
const pages = buildPageStack(document, sections, mainSurface);
|
|
247
|
+
const pages = buildPageStack(document, sections, mainSurface, measurementProvider);
|
|
246
248
|
const stories = resolvePageStories(pages);
|
|
247
|
-
const
|
|
249
|
+
const fragmentsByPageIndex = projectSurfaceBlocksToPageFragments(
|
|
250
|
+
mainSurface,
|
|
251
|
+
pages,
|
|
252
|
+
);
|
|
253
|
+
const graph = buildPageGraph({
|
|
254
|
+
pages,
|
|
255
|
+
sections,
|
|
256
|
+
stories,
|
|
257
|
+
fragmentsByPageIndex,
|
|
258
|
+
});
|
|
248
259
|
|
|
249
260
|
// Field dirtiness diff from previous graph
|
|
250
261
|
const dirtyFamilies = computeFieldDirtiness(cachedGraph, graph);
|
|
@@ -309,10 +320,16 @@ export function createLayoutEngine(
|
|
|
309
320
|
const sections = buildResolvedSections(document);
|
|
310
321
|
|
|
311
322
|
const dirtyPage = priorGraph.pages[firstDirty]!;
|
|
312
|
-
const freshSnapshots = buildPageStackFrom(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
323
|
+
const freshSnapshots = buildPageStackFrom(
|
|
324
|
+
document,
|
|
325
|
+
sections,
|
|
326
|
+
mainSurface,
|
|
327
|
+
{
|
|
328
|
+
startPageIndex: firstDirty,
|
|
329
|
+
startOffset: dirtyPage.startOffset,
|
|
330
|
+
},
|
|
331
|
+
measurementProvider,
|
|
332
|
+
);
|
|
316
333
|
|
|
317
334
|
// Convert fresh DocumentPageSnapshots into RuntimePageNodes via the
|
|
318
335
|
// standard buildPageGraph pipeline — this keeps region, story, and
|
|
@@ -543,13 +560,19 @@ function buildNavigationFromGraph(
|
|
|
543
560
|
): DocumentNavigationSnapshot {
|
|
544
561
|
const pages = deriveDocumentPageSnapshots(graph);
|
|
545
562
|
const sections = graph.sections;
|
|
563
|
+
const mainSurface = createEditorSurfaceSnapshot(
|
|
564
|
+
document,
|
|
565
|
+
createSelectionSnapshot(0, 0),
|
|
566
|
+
MAIN_STORY_TARGET,
|
|
567
|
+
);
|
|
568
|
+
const headings = buildHeadingOutline(document, mainSurface, sections, pages);
|
|
546
569
|
|
|
547
570
|
if (activeStory.kind === "main") {
|
|
548
571
|
const activePageIndex = deriveActivePageIndex(graph, selectionHead);
|
|
549
572
|
return {
|
|
550
573
|
pageCount: pages.length,
|
|
551
574
|
pages,
|
|
552
|
-
headings
|
|
575
|
+
headings,
|
|
553
576
|
activePageIndex,
|
|
554
577
|
activeSectionIndex: deriveActiveSectionIndex(graph, selectionHead),
|
|
555
578
|
};
|
|
@@ -564,24 +587,19 @@ function buildNavigationFromGraph(
|
|
|
564
587
|
return {
|
|
565
588
|
pageCount: pages.length,
|
|
566
589
|
pages,
|
|
567
|
-
headings
|
|
590
|
+
headings,
|
|
568
591
|
activePageIndex: firstPage >= 0 ? firstPage : 0,
|
|
569
592
|
activeSectionIndex: sectionIndex,
|
|
570
593
|
};
|
|
571
594
|
}
|
|
572
595
|
|
|
573
596
|
if (activeStory.kind === "footnote" || activeStory.kind === "endnote") {
|
|
574
|
-
const mainSurface = createEditorSurfaceSnapshot(
|
|
575
|
-
document,
|
|
576
|
-
createSelectionSnapshot(0, 0),
|
|
577
|
-
MAIN_STORY_TARGET,
|
|
578
|
-
);
|
|
579
597
|
const referencePosition = findNoteReferencePosition(mainSurface, activeStory);
|
|
580
598
|
const activePageIndex = deriveActivePageIndex(graph, referencePosition);
|
|
581
599
|
return {
|
|
582
600
|
pageCount: pages.length,
|
|
583
601
|
pages,
|
|
584
|
-
headings
|
|
602
|
+
headings,
|
|
585
603
|
activePageIndex,
|
|
586
604
|
activeSectionIndex:
|
|
587
605
|
graph.pages[activePageIndex]?.sectionIndex ??
|
|
@@ -592,20 +610,12 @@ function buildNavigationFromGraph(
|
|
|
592
610
|
return {
|
|
593
611
|
pageCount: pages.length,
|
|
594
612
|
pages,
|
|
595
|
-
headings
|
|
613
|
+
headings,
|
|
596
614
|
activePageIndex: 0,
|
|
597
615
|
activeSectionIndex: 0,
|
|
598
616
|
};
|
|
599
617
|
}
|
|
600
618
|
|
|
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
619
|
// ---------------------------------------------------------------------------
|
|
610
620
|
// Convenience: find the active page node directly
|
|
611
621
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
+
}
|