@beyondwork/docx-react-component 1.0.56 → 1.0.57
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 +157 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +17 -11
- package/src/core/selection/mapping.ts +18 -1
- package/src/core/selection/review-anchors.ts +29 -18
- package/src/io/chart-preview-resolver.ts +175 -41
- package/src/io/docx-session.ts +57 -2
- package/src/io/export/serialize-main-document.ts +82 -0
- package/src/io/export/serialize-styles.ts +61 -3
- package/src/io/export/table-properties-xml.ts +19 -4
- package/src/io/normalize/normalize-text.ts +33 -0
- package/src/io/ooxml/parse-anchor.ts +182 -0
- package/src/io/ooxml/parse-drawing.ts +319 -0
- package/src/io/ooxml/parse-fields.ts +115 -2
- package/src/io/ooxml/parse-fill.ts +215 -0
- package/src/io/ooxml/parse-font-table.ts +190 -0
- package/src/io/ooxml/parse-footnotes.ts +52 -1
- package/src/io/ooxml/parse-main-document.ts +241 -1
- package/src/io/ooxml/parse-numbering.ts +96 -0
- package/src/io/ooxml/parse-picture.ts +107 -0
- package/src/io/ooxml/parse-settings.ts +34 -0
- package/src/io/ooxml/parse-shapes.ts +87 -0
- package/src/io/ooxml/parse-solid-fill.ts +11 -0
- package/src/io/ooxml/parse-styles.ts +74 -1
- package/src/io/ooxml/parse-theme.ts +60 -0
- package/src/io/paste/html-clipboard.ts +449 -0
- package/src/io/paste/word-clipboard.ts +5 -1
- package/src/legal/_document-root.ts +26 -0
- package/src/legal/bookmarks.ts +4 -3
- package/src/legal/cross-references.ts +3 -2
- package/src/legal/defined-terms.ts +2 -1
- package/src/legal/signature-blocks.ts +2 -1
- package/src/model/canonical-document.ts +415 -3
- package/src/runtime/chart/chart-model-store.ts +73 -10
- package/src/runtime/document-runtime.ts +693 -41
- package/src/runtime/edit-ops/index.ts +129 -0
- package/src/runtime/event-refresh-hints.ts +7 -0
- package/src/runtime/field-resolver.ts +341 -0
- package/src/runtime/footnote-resolver.ts +55 -0
- package/src/runtime/hyperlink-color-resolver.ts +13 -10
- package/src/runtime/object-grab/index.ts +51 -0
- package/src/runtime/paragraph-style-resolver.ts +105 -0
- package/src/runtime/resolved-numbering-geometry.ts +12 -0
- package/src/runtime/selection/cursor-ops.ts +186 -15
- package/src/runtime/selection/index.ts +17 -1
- package/src/runtime/structure-ops/index.ts +77 -0
- package/src/runtime/styles-cascade.ts +33 -0
- package/src/runtime/surface-projection.ts +186 -12
- package/src/runtime/theme-color-resolver.ts +189 -44
- package/src/runtime/units.ts +46 -0
- package/src/runtime/view-state.ts +13 -2
- package/src/ui/WordReviewEditor.tsx +168 -10
- package/src/ui/editor-runtime-boundary.ts +94 -1
- package/src/ui/editor-shell-view.tsx +1 -1
- package/src/ui/runtime-shortcut-dispatch.ts +17 -3
- package/src/ui-tailwind/chart/ChartSurface.tsx +36 -10
- package/src/ui-tailwind/chart/layout/plot-area.ts +120 -45
- package/src/ui-tailwind/chart/render/area.tsx +22 -4
- package/src/ui-tailwind/chart/render/bar-column.tsx +37 -11
- package/src/ui-tailwind/chart/render/bubble.tsx +6 -2
- package/src/ui-tailwind/chart/render/combo.tsx +37 -4
- package/src/ui-tailwind/chart/render/line.tsx +28 -5
- package/src/ui-tailwind/chart/render/pie.tsx +36 -16
- package/src/ui-tailwind/chart/render/progressive-render.ts +8 -1
- package/src/ui-tailwind/chart/render/scatter.tsx +9 -4
- package/src/ui-tailwind/chrome/avatar-initials.ts +15 -0
- package/src/ui-tailwind/chrome/tw-comment-preview.tsx +3 -1
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +14 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +3 -2
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +30 -11
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +15 -2
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +1 -1
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +24 -7
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +31 -12
- package/src/ui-tailwind/chrome-overlay/page-border-resolver.ts +211 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +1 -0
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +74 -0
- package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +65 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-border-overlay.tsx +233 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +135 -13
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +51 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +12 -4
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +32 -12
- package/src/ui-tailwind/chrome-overlay/tw-toc-outline-sidebar.tsx +133 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +49 -10
- package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +119 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +236 -9
- package/src/ui-tailwind/editor-surface/pm-schema.ts +188 -11
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +28 -2
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +206 -0
- package/src/ui-tailwind/editor-surface/surface-layer.ts +66 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +29 -0
- package/src/ui-tailwind/editor-surface/tw-segment-view.tsx +7 -1
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +22 -6
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +10 -16
- package/src/ui-tailwind/review/tw-health-panel.tsx +0 -25
- package/src/ui-tailwind/review/tw-rail-card.tsx +38 -17
- package/src/ui-tailwind/review/tw-review-rail.tsx +2 -2
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +5 -12
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +2 -2
- package/src/ui-tailwind/theme/editor-theme.css +1 -0
- package/src/ui-tailwind/theme/tokens.css +6 -0
- package/src/ui-tailwind/theme/tokens.ts +10 -0
- package/src/validation/compatibility-engine.ts +2 -0
- package/src/validation/docx-comment-proof.ts +12 -3
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type {
|
|
14
|
+
CanonicalFontTable,
|
|
14
15
|
CanonicalParagraphFormatting,
|
|
15
16
|
CanonicalRunFormatting,
|
|
16
17
|
CharacterStyleDefinition,
|
|
17
18
|
ParagraphStyleDefinition,
|
|
18
19
|
StylesCatalog,
|
|
20
|
+
TableStyleDefinition,
|
|
19
21
|
} from "../model/canonical-document.ts";
|
|
20
22
|
|
|
21
23
|
export function resolveParagraphStyleChain(
|
|
@@ -192,3 +194,106 @@ export function resolveNumberingMarkerRunFormatting(
|
|
|
192
194
|
acc = mergeRun(acc, input.levelRunProperties);
|
|
193
195
|
return acc ?? {};
|
|
194
196
|
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Return the `w:next` style ID for `styleId` — the paragraph style Word
|
|
200
|
+
* applies after pressing Enter at the end of a paragraph of this style.
|
|
201
|
+
* Used by Lane 1's suggest-paragraph-split command.
|
|
202
|
+
*/
|
|
203
|
+
export function getNextStyleId(
|
|
204
|
+
styleId: string,
|
|
205
|
+
catalog: StylesCatalog | undefined,
|
|
206
|
+
): string | undefined {
|
|
207
|
+
return catalog?.paragraphs[styleId]?.nextStyle;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolve the effective font family name for a run.
|
|
212
|
+
*
|
|
213
|
+
* Walks the ECMA-376 §17.3.2.26 precedence for `<w:rFonts>`: `ascii` →
|
|
214
|
+
* `hAnsi` → `eastAsia` → `cs`. When none of those resolve on the direct
|
|
215
|
+
* run formatting, layers the paragraph/character style cascade for the
|
|
216
|
+
* same four fields, then falls back to the caller-supplied
|
|
217
|
+
* `themeMinorFont` (ECMA-376 default for body text).
|
|
218
|
+
*
|
|
219
|
+
* `fontTable`, when supplied, is consulted only for presence — an entry
|
|
220
|
+
* whose `w:name` does not appear in `fontTable.fonts` is still returned
|
|
221
|
+
* as-is (Word behavior: substitute at render time, not at model resolve).
|
|
222
|
+
* Lane 3a's measurement backend owns the substitution decision.
|
|
223
|
+
*
|
|
224
|
+
* Returns undefined only when no source in the cascade supplies a family
|
|
225
|
+
* name (rare — production docs always hit docDefaults or the theme).
|
|
226
|
+
*/
|
|
227
|
+
export function resolveRunFontFamily(
|
|
228
|
+
input: RunResolveInput,
|
|
229
|
+
catalog: StylesCatalog | undefined,
|
|
230
|
+
themeMinorFont: string | undefined,
|
|
231
|
+
fontTable?: CanonicalFontTable | undefined,
|
|
232
|
+
): string | undefined {
|
|
233
|
+
// `fontTable` is currently unused inside this resolver; the ECMA-376
|
|
234
|
+
// rFonts precedence does not require consulting the package's fontTable
|
|
235
|
+
// to pick a name. It is reserved so Lane 3a's measurement backend can
|
|
236
|
+
// layer substitution (e.g. pick a monospace fallback when the resolved
|
|
237
|
+
// name has `pitch === "fixed"`) by threading the same table through.
|
|
238
|
+
void fontTable;
|
|
239
|
+
const resolved = resolveEffectiveRunFormatting(input, catalog);
|
|
240
|
+
const name =
|
|
241
|
+
resolved.fontFamilyAscii ??
|
|
242
|
+
resolved.fontFamilyHAnsi ??
|
|
243
|
+
resolved.fontFamilyEastAsia ??
|
|
244
|
+
resolved.fontFamilyCs ??
|
|
245
|
+
resolved.fontFamily ??
|
|
246
|
+
themeMinorFont;
|
|
247
|
+
return name;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveTableStyleChain(
|
|
251
|
+
styleId: string,
|
|
252
|
+
catalog: StylesCatalog | undefined,
|
|
253
|
+
): TableStyleDefinition[] {
|
|
254
|
+
const chain: TableStyleDefinition[] = [];
|
|
255
|
+
if (!catalog) return chain;
|
|
256
|
+
const visited = new Set<string>();
|
|
257
|
+
let current: string | undefined = styleId;
|
|
258
|
+
while (current && !visited.has(current)) {
|
|
259
|
+
visited.add(current);
|
|
260
|
+
const def: TableStyleDefinition | undefined = catalog.tables[current];
|
|
261
|
+
if (!def) break;
|
|
262
|
+
chain.push(def);
|
|
263
|
+
current = def.basedOn;
|
|
264
|
+
}
|
|
265
|
+
return chain;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Resolve the paragraph + run formatting that applies to a cell's body text
|
|
270
|
+
* when only the table style is known (no cell-paragraph-level overrides).
|
|
271
|
+
*
|
|
272
|
+
* Cascade (lowest to highest priority):
|
|
273
|
+
* 1. `catalog.docDefaults.paragraph` / `.run`
|
|
274
|
+
* 2. Table style's `basedOn` chain, walked root-to-leaf, projecting each
|
|
275
|
+
* style's `formatting.paragraphProperties` / `.runProperties`.
|
|
276
|
+
*
|
|
277
|
+
* Conditional-region pPr/rPr (`<w:tblStylePr w:type="...">`) is intentionally
|
|
278
|
+
* NOT layered here — callers that need region-aware cell-text cascade must
|
|
279
|
+
* extend this once the conditional region parser captures pPr/rPr (Lane 3a
|
|
280
|
+
* follow-up). Per-paragraph and per-run formatting from the cell's content
|
|
281
|
+
* is layered by `resolveEffectiveParagraphFormatting` /
|
|
282
|
+
* `resolveEffectiveRunFormatting`; this function only returns the floor.
|
|
283
|
+
*/
|
|
284
|
+
export function resolveTableCellTextFormatting(
|
|
285
|
+
tableStyleId: string | undefined,
|
|
286
|
+
catalog: StylesCatalog | undefined,
|
|
287
|
+
): { paragraph: CanonicalParagraphFormatting; run: CanonicalRunFormatting } {
|
|
288
|
+
let paragraph: CanonicalParagraphFormatting | undefined = catalog?.docDefaults?.paragraph;
|
|
289
|
+
let run: CanonicalRunFormatting | undefined = catalog?.docDefaults?.run;
|
|
290
|
+
if (catalog && tableStyleId) {
|
|
291
|
+
const chain = resolveTableStyleChain(tableStyleId, catalog);
|
|
292
|
+
for (let i = chain.length - 1; i >= 0; i -= 1) {
|
|
293
|
+
const formatting = chain[i]!.formatting;
|
|
294
|
+
paragraph = mergeParagraph(paragraph, formatting?.paragraphProperties);
|
|
295
|
+
run = mergeRun(run, formatting?.runProperties);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return { paragraph: paragraph ?? {}, run: run ?? {} };
|
|
299
|
+
}
|
|
@@ -12,16 +12,28 @@ import type {
|
|
|
12
12
|
|
|
13
13
|
export const DEFAULT_NUMBERING_START_AT = 1;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Resolved numbering geometry. **All numeric distance values are in OOXML
|
|
17
|
+
* twips (1/1440 in)** — the same unit `w:ind`/`w:tabs`/`w:spacing` carry on
|
|
18
|
+
* the parse side. Render consumers convert to display units at the boundary
|
|
19
|
+
* (`pt = twips / 20`, `px = twips × FRAME_PX_PER_TWIP_AT_96DPI`) — see
|
|
20
|
+
* `src/ui-tailwind/editor-surface/pm-schema.ts:331` and
|
|
21
|
+
* `tw-page-block-view.helpers.ts:179` for the canonical conversion sites.
|
|
22
|
+
* Do not pre-convert here; the runtime stays in OOXML units so cache keys
|
|
23
|
+
* and serializer round-trips stay byte-identical.
|
|
24
|
+
*/
|
|
15
25
|
export interface ResolvedNumberingGeometry {
|
|
16
26
|
markerJustification?: NumberingLevelParagraphGeometry["justification"];
|
|
17
27
|
spacing?: ParagraphSpacing;
|
|
18
28
|
indentation?: ParagraphIndentation;
|
|
19
29
|
tabStops?: TabStop[];
|
|
30
|
+
/** All fields in twips. */
|
|
20
31
|
markerLane?: {
|
|
21
32
|
start: number;
|
|
22
33
|
width: number;
|
|
23
34
|
textStart: number;
|
|
24
35
|
};
|
|
36
|
+
/** All fields in twips. */
|
|
25
37
|
textColumn?: {
|
|
26
38
|
start: number;
|
|
27
39
|
right?: number;
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* R.1
|
|
2
|
+
* R.1 SelectionLayer cursor primitives.
|
|
3
|
+
*
|
|
4
|
+
* Phase 6a (shipped) — layout-independent primitives over
|
|
5
|
+
* `(DocumentRootNode, CursorSelection, op) → CursorSelection` using the
|
|
6
|
+
* canonical story layer (`createPlainText(parseTextStory(doc))`) and
|
|
7
|
+
* `Intl.Segmenter` for word boundaries.
|
|
8
|
+
*
|
|
9
|
+
* Phase 6b (this module) — layout-aware primitives (`moveUp` / `moveDown` /
|
|
10
|
+
* `moveLineStart` / `moveLineEnd`) that consult a `RenderAnchorIndex` from
|
|
11
|
+
* Lane 3a P9 to resolve line boundaries. When no index is supplied the
|
|
12
|
+
* primitives degrade gracefully (see per-function docs).
|
|
3
13
|
*
|
|
4
|
-
* Pure functions over `(DocumentRootNode, CursorSelection, op) → CursorSelection`.
|
|
5
14
|
* `extend: true` keeps the anchor fixed and moves only the head — the
|
|
6
15
|
* LibreOffice `SwPaM` head/anchor split that matches Shift+arrow semantics.
|
|
7
16
|
*
|
|
8
|
-
* Positions are 0-based logical positions per the canonical story layer
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* wide. The plain-text string produced by `createPlainText` is a 1:1 mapping
|
|
12
|
-
* of those logical positions to characters, which makes position-math trivial
|
|
13
|
-
* and makes `Intl.Segmenter` (for word boundaries) a drop-in fit.
|
|
14
|
-
*
|
|
15
|
-
* What this module deliberately does NOT ship yet:
|
|
16
|
-
* - `moveUp` / `moveDown` — genuinely layout-dependent (need column tracking
|
|
17
|
-
* + line-wrap info). Follows Phase 6b once Lane 3a P9 exposes the per-run
|
|
18
|
-
* layout facet needed for column-preserving movement.
|
|
19
|
-
* - `moveLineStart` / `moveLineEnd` — same; these need soft-wrap info that
|
|
20
|
-
* the canonical story layer does not expose.
|
|
17
|
+
* Positions are 0-based logical positions per the canonical story layer.
|
|
18
|
+
* Scope markers are zero-width; paragraph breaks are 1 wide; text / tab /
|
|
19
|
+
* hard_break / image / opaque are 1 wide.
|
|
21
20
|
*/
|
|
22
21
|
|
|
23
22
|
import { createPlainText, parseTextStory } from "../../core/schema/text-schema.ts";
|
|
24
23
|
import type { DocumentRootNode } from "../../model/canonical-document.ts";
|
|
24
|
+
import type { RenderAnchorIndex, RenderFrameRect } from "../render/render-frame-types.ts";
|
|
25
25
|
|
|
26
26
|
export interface CursorSelection {
|
|
27
27
|
anchor: number;
|
|
@@ -35,6 +35,13 @@ export interface CursorMoveOptions {
|
|
|
35
35
|
* anchor and head land at the new head, collapsing any range selection.
|
|
36
36
|
*/
|
|
37
37
|
extend?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* R.1 Phase 6b — optional. Layout-aware primitives
|
|
40
|
+
* (`moveUp` / `moveDown` / `moveLineStart` / `moveLineEnd`) consult this
|
|
41
|
+
* index to resolve line boundaries via `byRuntimeOffset(offset)`. Absent
|
|
42
|
+
* in pure unit-test contexts; primitives degrade gracefully without it.
|
|
43
|
+
*/
|
|
44
|
+
anchorIndex?: RenderAnchorIndex;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
export function moveCharLeft(
|
|
@@ -148,6 +155,170 @@ export function moveWordLeft(
|
|
|
148
155
|
return finalize(selection, prevBoundary, options);
|
|
149
156
|
}
|
|
150
157
|
|
|
158
|
+
// ─── Phase 6b — layout-aware primitives ────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Move the head to the character on the next line at the nearest column X
|
|
162
|
+
* to the current head. Requires an anchor index; without one, this is a
|
|
163
|
+
* no-op (graceful degradation for pure unit-test contexts).
|
|
164
|
+
*
|
|
165
|
+
* Algorithm: probe the current head's rect for its `topPx` + `leftPx` (the
|
|
166
|
+
* anchor column). Scan forward through offsets; the first offset whose
|
|
167
|
+
* rect has a strictly-greater `topPx` marks the start of the next line.
|
|
168
|
+
* From that offset, continue while `topPx` matches the next-line top,
|
|
169
|
+
* tracking which offset has the smallest `|leftPx - anchorLeftPx|`.
|
|
170
|
+
*/
|
|
171
|
+
export function moveDown(
|
|
172
|
+
doc: DocumentRootNode,
|
|
173
|
+
selection: CursorSelection,
|
|
174
|
+
options: CursorMoveOptions = {},
|
|
175
|
+
): CursorSelection {
|
|
176
|
+
const index = options.anchorIndex;
|
|
177
|
+
if (!index) return finalize(selection, selection.head, options);
|
|
178
|
+
|
|
179
|
+
const text = documentPlainText(doc);
|
|
180
|
+
const startHead = options.extend
|
|
181
|
+
? selection.head
|
|
182
|
+
: Math.max(selection.anchor, selection.head);
|
|
183
|
+
const currentRect = index.byRuntimeOffset(startHead);
|
|
184
|
+
if (!currentRect) return finalize(selection, startHead, options);
|
|
185
|
+
|
|
186
|
+
const anchorLeft = currentRect.leftPx;
|
|
187
|
+
const currentTop = currentRect.topPx;
|
|
188
|
+
|
|
189
|
+
// Find first offset with a different (higher) topPx.
|
|
190
|
+
let nextLineStart = -1;
|
|
191
|
+
let nextLineTop = Number.POSITIVE_INFINITY;
|
|
192
|
+
for (let i = startHead + 1; i <= text.length; i += 1) {
|
|
193
|
+
const r = index.byRuntimeOffset(i);
|
|
194
|
+
if (!r) continue;
|
|
195
|
+
if (r.topPx > currentTop) {
|
|
196
|
+
nextLineStart = i;
|
|
197
|
+
nextLineTop = r.topPx;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (nextLineStart < 0) return finalize(selection, startHead, options);
|
|
202
|
+
|
|
203
|
+
// Scan the next line for the offset with leftPx closest to anchorLeft.
|
|
204
|
+
let bestOffset = nextLineStart;
|
|
205
|
+
let bestDelta = Math.abs((index.byRuntimeOffset(nextLineStart)?.leftPx ?? 0) - anchorLeft);
|
|
206
|
+
for (let i = nextLineStart + 1; i <= text.length; i += 1) {
|
|
207
|
+
const r = index.byRuntimeOffset(i);
|
|
208
|
+
if (!r || r.topPx !== nextLineTop) break;
|
|
209
|
+
const delta = Math.abs(r.leftPx - anchorLeft);
|
|
210
|
+
if (delta < bestDelta) {
|
|
211
|
+
bestDelta = delta;
|
|
212
|
+
bestOffset = i;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return finalize(selection, bestOffset, options);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Mirror of `moveDown`, scanning upward. */
|
|
219
|
+
export function moveUp(
|
|
220
|
+
doc: DocumentRootNode,
|
|
221
|
+
selection: CursorSelection,
|
|
222
|
+
options: CursorMoveOptions = {},
|
|
223
|
+
): CursorSelection {
|
|
224
|
+
const index = options.anchorIndex;
|
|
225
|
+
if (!index) return finalize(selection, selection.head, options);
|
|
226
|
+
|
|
227
|
+
const startHead = options.extend
|
|
228
|
+
? selection.head
|
|
229
|
+
: Math.min(selection.anchor, selection.head);
|
|
230
|
+
const currentRect = index.byRuntimeOffset(startHead);
|
|
231
|
+
if (!currentRect) return finalize(selection, startHead, options);
|
|
232
|
+
|
|
233
|
+
const anchorLeft = currentRect.leftPx;
|
|
234
|
+
const currentTop = currentRect.topPx;
|
|
235
|
+
|
|
236
|
+
let prevLineEnd = -1;
|
|
237
|
+
let prevLineTop = Number.NEGATIVE_INFINITY;
|
|
238
|
+
for (let i = startHead - 1; i >= 0; i -= 1) {
|
|
239
|
+
const r = index.byRuntimeOffset(i);
|
|
240
|
+
if (!r) continue;
|
|
241
|
+
if (r.topPx < currentTop) {
|
|
242
|
+
prevLineEnd = i;
|
|
243
|
+
prevLineTop = r.topPx;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (prevLineEnd < 0) return finalize(selection, startHead, options);
|
|
248
|
+
|
|
249
|
+
let bestOffset = prevLineEnd;
|
|
250
|
+
let bestDelta = Math.abs((index.byRuntimeOffset(prevLineEnd)?.leftPx ?? 0) - anchorLeft);
|
|
251
|
+
for (let i = prevLineEnd - 1; i >= 0; i -= 1) {
|
|
252
|
+
const r = index.byRuntimeOffset(i);
|
|
253
|
+
if (!r || r.topPx !== prevLineTop) break;
|
|
254
|
+
const delta = Math.abs(r.leftPx - anchorLeft);
|
|
255
|
+
if (delta < bestDelta) {
|
|
256
|
+
bestDelta = delta;
|
|
257
|
+
bestOffset = i;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return finalize(selection, bestOffset, options);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Move the head to the first offset on the current visual line. Requires an
|
|
265
|
+
* anchor index; without one, collapses to document start (offset 0), which
|
|
266
|
+
* is a common keyboard fallback (matches "Home" at the start of an unwrapped
|
|
267
|
+
* document).
|
|
268
|
+
*/
|
|
269
|
+
export function moveLineStart(
|
|
270
|
+
doc: DocumentRootNode,
|
|
271
|
+
selection: CursorSelection,
|
|
272
|
+
options: CursorMoveOptions = {},
|
|
273
|
+
): CursorSelection {
|
|
274
|
+
const index = options.anchorIndex;
|
|
275
|
+
if (!index) return finalize(selection, 0, options);
|
|
276
|
+
|
|
277
|
+
const startHead = selection.head;
|
|
278
|
+
const currentRect = index.byRuntimeOffset(startHead);
|
|
279
|
+
if (!currentRect) return finalize(selection, startHead, options);
|
|
280
|
+
|
|
281
|
+
const currentTop = currentRect.topPx;
|
|
282
|
+
let lineStart = startHead;
|
|
283
|
+
for (let i = startHead - 1; i >= 0; i -= 1) {
|
|
284
|
+
const r = index.byRuntimeOffset(i);
|
|
285
|
+
if (!r) continue;
|
|
286
|
+
if (r.topPx !== currentTop) break;
|
|
287
|
+
lineStart = i;
|
|
288
|
+
}
|
|
289
|
+
return finalize(selection, lineStart, options);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Move the head to the last offset on the current visual line. Without an
|
|
294
|
+
* anchor index, collapses to document end.
|
|
295
|
+
*/
|
|
296
|
+
export function moveLineEnd(
|
|
297
|
+
doc: DocumentRootNode,
|
|
298
|
+
selection: CursorSelection,
|
|
299
|
+
options: CursorMoveOptions = {},
|
|
300
|
+
): CursorSelection {
|
|
301
|
+
const text = documentPlainText(doc);
|
|
302
|
+
const index = options.anchorIndex;
|
|
303
|
+
if (!index) return finalize(selection, text.length, options);
|
|
304
|
+
|
|
305
|
+
const startHead = selection.head;
|
|
306
|
+
const currentRect = index.byRuntimeOffset(startHead);
|
|
307
|
+
if (!currentRect) return finalize(selection, startHead, options);
|
|
308
|
+
|
|
309
|
+
const currentTop = currentRect.topPx;
|
|
310
|
+
let lineEnd = startHead;
|
|
311
|
+
for (let i = startHead + 1; i <= text.length; i += 1) {
|
|
312
|
+
const r = index.byRuntimeOffset(i);
|
|
313
|
+
if (!r) continue;
|
|
314
|
+
if (r.topPx !== currentTop) break;
|
|
315
|
+
lineEnd = i;
|
|
316
|
+
}
|
|
317
|
+
return finalize(selection, lineEnd, options);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
321
|
+
|
|
151
322
|
function finalize(
|
|
152
323
|
selection: CursorSelection,
|
|
153
324
|
head: number,
|
|
@@ -15,8 +15,12 @@ import type { CanonicalDocumentEnvelope, SelectionSnapshot } from "../../core/st
|
|
|
15
15
|
import {
|
|
16
16
|
moveCharLeft,
|
|
17
17
|
moveCharRight,
|
|
18
|
+
moveDown,
|
|
19
|
+
moveLineEnd,
|
|
20
|
+
moveLineStart,
|
|
18
21
|
moveParagraphEnd,
|
|
19
22
|
moveParagraphStart,
|
|
23
|
+
moveUp,
|
|
20
24
|
moveWordLeft,
|
|
21
25
|
moveWordRight,
|
|
22
26
|
type CursorMoveOptions,
|
|
@@ -39,7 +43,11 @@ export type CursorMoveOp =
|
|
|
39
43
|
| "word-left"
|
|
40
44
|
| "word-right"
|
|
41
45
|
| "paragraph-start"
|
|
42
|
-
| "paragraph-end"
|
|
46
|
+
| "paragraph-end"
|
|
47
|
+
| "up"
|
|
48
|
+
| "down"
|
|
49
|
+
| "line-start"
|
|
50
|
+
| "line-end";
|
|
43
51
|
|
|
44
52
|
export interface SelectionLayer {
|
|
45
53
|
/**
|
|
@@ -83,6 +91,14 @@ export const selectionLayer: SelectionLayer = {
|
|
|
83
91
|
return moveParagraphStart(doc, selection, options);
|
|
84
92
|
case "paragraph-end":
|
|
85
93
|
return moveParagraphEnd(doc, selection, options);
|
|
94
|
+
case "up":
|
|
95
|
+
return moveUp(doc, selection, options);
|
|
96
|
+
case "down":
|
|
97
|
+
return moveDown(doc, selection, options);
|
|
98
|
+
case "line-start":
|
|
99
|
+
return moveLineStart(doc, selection, options);
|
|
100
|
+
case "line-end":
|
|
101
|
+
return moveLineEnd(doc, selection, options);
|
|
86
102
|
}
|
|
87
103
|
},
|
|
88
104
|
validate(doc, selection, maxOffset) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R.3 StructureLayer — named shell module for structural mutations. See
|
|
3
|
+
* `docs/plans/lane-1-editing-foundation.md` §R.3.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors R.2 EditLayer's pattern: a thin named entry point over existing
|
|
6
|
+
* pure functions. The layer's purpose is to be the single site where callers
|
|
7
|
+
* reach for table / list / section / fragment-splice concerns, so R.5.b
|
|
8
|
+
* (post-edit validation hook) and R.5.a (action bracketing) can attach to a
|
|
9
|
+
* single seam instead of a fanout of helper call-sites.
|
|
10
|
+
*
|
|
11
|
+
* Phase 6/Item-2 scope: expose `applyFragmentInsert` (shipped in I2 Tier B
|
|
12
|
+
* Slice 1). Adding list/table/section-op wrappers is deferred because those
|
|
13
|
+
* existing command helpers already have rich type surfaces that don't need a
|
|
14
|
+
* named re-export to be callable — reach for them directly via
|
|
15
|
+
* `src/core/commands/{list,table,section}-commands.ts` until a specific case
|
|
16
|
+
* surfaces requiring the seam. Keeping this interface minimal avoids the
|
|
17
|
+
* "enumeration trap" that larger shell refactors fall into.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { CanonicalDocumentFragment } from "../../api/public-types.ts";
|
|
21
|
+
import type {
|
|
22
|
+
CanonicalDocumentEnvelope,
|
|
23
|
+
SelectionSnapshot,
|
|
24
|
+
} from "../../core/state/editor-state.ts";
|
|
25
|
+
import type { StructuralMutationResult } from "../../core/commands/structural-helpers.ts";
|
|
26
|
+
import type { TextCommandContext } from "../../core/commands/text-commands.ts";
|
|
27
|
+
import { countLogicalPositions, parseTextStory } from "../../core/schema/text-schema.ts";
|
|
28
|
+
import { validateSelectionAgainstDocument } from "../selection/post-edit-validator.ts";
|
|
29
|
+
import { applyFragmentInsert } from "./fragment-insert.ts";
|
|
30
|
+
|
|
31
|
+
export interface StructureLayer {
|
|
32
|
+
/**
|
|
33
|
+
* Splice a `CanonicalDocumentFragment` at the current selection. Baseline
|
|
34
|
+
* semantic: the caret paragraph is split; fragment blocks are inserted
|
|
35
|
+
* between the two halves; range selections are deleted first. Empty
|
|
36
|
+
* fragments are no-ops.
|
|
37
|
+
*/
|
|
38
|
+
applyFragmentInsert(
|
|
39
|
+
doc: CanonicalDocumentEnvelope,
|
|
40
|
+
selection: SelectionSnapshot,
|
|
41
|
+
fragment: CanonicalDocumentFragment,
|
|
42
|
+
context: TextCommandContext,
|
|
43
|
+
): StructuralMutationResult;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* R.5.b — every StructureLayer return passes through the post-edit selection
|
|
48
|
+
* validator before leaving the layer. Structural mutations can legitimately
|
|
49
|
+
* shrink the document (fragment with empty trailing paragraphs, etc.), so
|
|
50
|
+
* clamping here is a defense-in-depth hook matching the EditLayer's.
|
|
51
|
+
*
|
|
52
|
+
* `StructuralMutationResult` doesn't carry a pre-computed `storyText`, so
|
|
53
|
+
* we compute the post-mutation story size via `countLogicalPositions` over
|
|
54
|
+
* the parsed story. That's one parse per structural op — acceptable; these
|
|
55
|
+
* ops are off the typing hot path.
|
|
56
|
+
*/
|
|
57
|
+
function validateResult(result: StructuralMutationResult): StructuralMutationResult {
|
|
58
|
+
if (!result.changed) {
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
const story = parseTextStory(result.document.content);
|
|
62
|
+
const maxOffset = countLogicalPositions(story.units);
|
|
63
|
+
const validated = validateSelectionAgainstDocument(result.document, result.selection, maxOffset);
|
|
64
|
+
if (validated === result.selection) {
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
return { ...result, selection: validated };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Default stateless StructureLayer instance. Safe to share across runtimes.
|
|
72
|
+
*/
|
|
73
|
+
export const structureLayer: StructureLayer = {
|
|
74
|
+
applyFragmentInsert(doc, selection, fragment, context) {
|
|
75
|
+
return validateResult(applyFragmentInsert(doc, selection, fragment, context));
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central entry point for OOXML style cascade resolution.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the paragraph, character, run, numbering-marker, and table style
|
|
5
|
+
* resolvers from their individual modules so downstream consumers (Lane 3a
|
|
6
|
+
* measurement, Lane 1 style picker, agent tooling) can import from a single,
|
|
7
|
+
* stable location.
|
|
8
|
+
*
|
|
9
|
+
* Adding a new resolver? Add it to its feature module (paragraph, table, etc.)
|
|
10
|
+
* and re-export from here. Do not define new cascade logic in this file.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
resolveParagraphStyleChain,
|
|
15
|
+
resolveCharacterStyleChain,
|
|
16
|
+
resolveEffectiveParagraphFormatting,
|
|
17
|
+
resolveEffectiveRunFormatting,
|
|
18
|
+
resolveNumberingMarkerRunFormatting,
|
|
19
|
+
resolveTableCellTextFormatting,
|
|
20
|
+
resolveRunFontFamily,
|
|
21
|
+
getNextStyleId,
|
|
22
|
+
type ParagraphResolveInput,
|
|
23
|
+
type RunResolveInput,
|
|
24
|
+
type MarkerResolveInput,
|
|
25
|
+
} from "./paragraph-style-resolver.ts";
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
resolveTableStyleResolution,
|
|
29
|
+
type ResolvedTableCellStyle,
|
|
30
|
+
type ResolvedTableRowStyle,
|
|
31
|
+
type ResolvedTableLevelProperties,
|
|
32
|
+
type ResolvedTableStyleResolution,
|
|
33
|
+
} from "./table-style-resolver.ts";
|