smartrte-core 0.2.1 → 1.0.0-beta.2
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/CHANGELOG.md +16 -0
- package/README.md +23 -0
- package/dist/command.d.ts +8 -8
- package/dist/editor.d.ts +32 -0
- package/dist/editor.js +121 -0
- package/dist/foundation/annotations/index.d.ts +2 -0
- package/dist/foundation/annotations/index.js +2 -0
- package/dist/foundation/annotations/range.d.ts +54 -0
- package/dist/foundation/annotations/range.js +158 -0
- package/dist/foundation/annotations/types.d.ts +33 -0
- package/dist/foundation/annotations/types.js +1 -0
- package/dist/foundation/atom/commands.d.ts +11 -0
- package/dist/foundation/atom/commands.js +118 -0
- package/dist/foundation/atom/composition.d.ts +8 -0
- package/dist/foundation/atom/composition.js +22 -0
- package/dist/foundation/atom/declarations.d.ts +2 -0
- package/dist/foundation/atom/declarations.js +17 -0
- package/dist/foundation/atom/formats.d.ts +24 -0
- package/dist/foundation/atom/formats.js +105 -0
- package/dist/foundation/atom/index.d.ts +10 -0
- package/dist/foundation/atom/index.js +10 -0
- package/dist/foundation/atom/lifecycle.d.ts +12 -0
- package/dist/foundation/atom/lifecycle.js +41 -0
- package/dist/foundation/atom/persistence.d.ts +3 -0
- package/dist/foundation/atom/persistence.js +20 -0
- package/dist/foundation/atom/plugin.d.ts +20 -0
- package/dist/foundation/atom/plugin.js +55 -0
- package/dist/foundation/atom/schema.d.ts +3 -0
- package/dist/foundation/atom/schema.js +29 -0
- package/dist/foundation/atom/security.d.ts +10 -0
- package/dist/foundation/atom/security.js +20 -0
- package/dist/foundation/atom/selection.d.ts +3 -0
- package/dist/foundation/atom/selection.js +4 -0
- package/dist/foundation/atom/types.d.ts +47 -0
- package/dist/foundation/atom/types.js +1 -0
- package/dist/foundation/block/commands.d.ts +17 -0
- package/dist/foundation/block/commands.js +197 -0
- package/dist/foundation/block/formats.d.ts +33 -0
- package/dist/foundation/block/formats.js +57 -0
- package/dist/foundation/block/index.d.ts +5 -0
- package/dist/foundation/block/index.js +5 -0
- package/dist/foundation/block/input.d.ts +19 -0
- package/dist/foundation/block/input.js +67 -0
- package/dist/foundation/block/plugin.d.ts +26 -0
- package/dist/foundation/block/plugin.js +79 -0
- package/dist/foundation/block/schema.d.ts +49 -0
- package/dist/foundation/block/schema.js +27 -0
- package/dist/foundation/block/types.d.ts +40 -0
- package/dist/foundation/block/types.js +1 -0
- package/dist/foundation/boundaries.d.ts +11 -0
- package/dist/foundation/boundaries.js +117 -0
- package/dist/foundation/clipboard/detection.d.ts +3 -0
- package/dist/foundation/clipboard/detection.js +29 -0
- package/dist/foundation/clipboard/diagnostics.d.ts +24 -0
- package/dist/foundation/clipboard/diagnostics.js +59 -0
- package/dist/foundation/clipboard/index.d.ts +8 -0
- package/dist/foundation/clipboard/index.js +8 -0
- package/dist/foundation/clipboard/insertion.d.ts +22 -0
- package/dist/foundation/clipboard/insertion.js +210 -0
- package/dist/foundation/clipboard/normalizers.d.ts +8 -0
- package/dist/foundation/clipboard/normalizers.js +229 -0
- package/dist/foundation/clipboard/pipeline.d.ts +13 -0
- package/dist/foundation/clipboard/pipeline.js +58 -0
- package/dist/foundation/clipboard/sanitizer.d.ts +3 -0
- package/dist/foundation/clipboard/sanitizer.js +58 -0
- package/dist/foundation/clipboard/serialization.d.ts +5 -0
- package/dist/foundation/clipboard/serialization.js +33 -0
- package/dist/foundation/clipboard/transfer.d.ts +11 -0
- package/dist/foundation/clipboard/transfer.js +108 -0
- package/dist/foundation/clipboard/types.d.ts +65 -0
- package/dist/foundation/clipboard/types.js +1 -0
- package/dist/foundation/collab/index.d.ts +3 -0
- package/dist/foundation/collab/index.js +3 -0
- package/dist/foundation/collab/rebase.d.ts +28 -0
- package/dist/foundation/collab/rebase.js +82 -0
- package/dist/foundation/collab/transform.d.ts +73 -0
- package/dist/foundation/collab/transform.js +251 -0
- package/dist/foundation/collab/types.d.ts +52 -0
- package/dist/foundation/collab/types.js +1 -0
- package/dist/foundation/comments/fromSelection.d.ts +16 -0
- package/dist/foundation/comments/fromSelection.js +24 -0
- package/dist/foundation/comments/index.d.ts +3 -0
- package/dist/foundation/comments/index.js +3 -0
- package/dist/foundation/comments/rebase.d.ts +31 -0
- package/dist/foundation/comments/rebase.js +43 -0
- package/dist/foundation/comments/types.d.ts +37 -0
- package/dist/foundation/comments/types.js +1 -0
- package/dist/foundation/diff/documentDiff.d.ts +12 -0
- package/dist/foundation/diff/documentDiff.js +148 -0
- package/dist/foundation/diff/index.d.ts +3 -0
- package/dist/foundation/diff/index.js +3 -0
- package/dist/foundation/diff/textDiff.d.ts +12 -0
- package/dist/foundation/diff/textDiff.js +114 -0
- package/dist/foundation/diff/types.d.ts +74 -0
- package/dist/foundation/diff/types.js +1 -0
- package/dist/foundation/editor.d.ts +138 -0
- package/dist/foundation/editor.js +398 -0
- package/dist/foundation/formats/codec.d.ts +45 -0
- package/dist/foundation/formats/codec.js +1 -0
- package/dist/foundation/formats/docx/export.d.ts +4 -0
- package/dist/foundation/formats/docx/export.js +285 -0
- package/dist/foundation/formats/docx/import.d.ts +9 -0
- package/dist/foundation/formats/docx/import.js +37 -0
- package/dist/foundation/formats/docx/nestingGuard.d.ts +40 -0
- package/dist/foundation/formats/docx/nestingGuard.js +66 -0
- package/dist/foundation/formats/docx/portableAtoms.d.ts +4 -0
- package/dist/foundation/formats/docx/portableAtoms.js +66 -0
- package/dist/foundation/formats/docx/styledImport.d.ts +10 -0
- package/dist/foundation/formats/docx/styledImport.js +205 -0
- package/dist/foundation/formats/featureCodecs.d.ts +37 -0
- package/dist/foundation/formats/featureCodecs.js +69 -0
- package/dist/foundation/formats/fidelity.d.ts +31 -0
- package/dist/foundation/formats/fidelity.js +123 -0
- package/dist/foundation/formats/index.d.ts +7 -0
- package/dist/foundation/formats/index.js +7 -0
- package/dist/foundation/formats/pdf/format.d.ts +29 -0
- package/dist/foundation/formats/pdf/format.js +190 -0
- package/dist/foundation/history.d.ts +20 -0
- package/dist/foundation/history.js +121 -0
- package/dist/foundation/identity.d.ts +8 -0
- package/dist/foundation/identity.js +30 -0
- package/dist/foundation/index.d.ts +30 -0
- package/dist/foundation/index.js +30 -0
- package/dist/foundation/list/commands.d.ts +28 -0
- package/dist/foundation/list/commands.js +422 -0
- package/dist/foundation/list/formats.d.ts +21 -0
- package/dist/foundation/list/formats.js +813 -0
- package/dist/foundation/list/index.d.ts +8 -0
- package/dist/foundation/list/index.js +8 -0
- package/dist/foundation/list/input.d.ts +35 -0
- package/dist/foundation/list/input.js +320 -0
- package/dist/foundation/list/plugin.d.ts +21 -0
- package/dist/foundation/list/plugin.js +71 -0
- package/dist/foundation/list/presets.d.ts +16 -0
- package/dist/foundation/list/presets.js +21 -0
- package/dist/foundation/list/schema.d.ts +35 -0
- package/dist/foundation/list/schema.js +29 -0
- package/dist/foundation/list/shadow.d.ts +54 -0
- package/dist/foundation/list/shadow.js +75 -0
- package/dist/foundation/list/shadowCorpus.d.ts +10 -0
- package/dist/foundation/list/shadowCorpus.js +41 -0
- package/dist/foundation/list/types.d.ts +50 -0
- package/dist/foundation/list/types.js +1 -0
- package/dist/foundation/mapping.d.ts +11 -0
- package/dist/foundation/mapping.js +40 -0
- package/dist/foundation/marks/canonical.d.ts +9 -0
- package/dist/foundation/marks/canonical.js +72 -0
- package/dist/foundation/marks/commands.d.ts +21 -0
- package/dist/foundation/marks/commands.js +136 -0
- package/dist/foundation/marks/dom.d.ts +2 -0
- package/dist/foundation/marks/dom.js +46 -0
- package/dist/foundation/marks/editor.d.ts +8 -0
- package/dist/foundation/marks/editor.js +50 -0
- package/dist/foundation/marks/formats.d.ts +37 -0
- package/dist/foundation/marks/formats.js +49 -0
- package/dist/foundation/marks/hardBreak.d.ts +8 -0
- package/dist/foundation/marks/hardBreak.js +80 -0
- package/dist/foundation/marks/index.d.ts +11 -0
- package/dist/foundation/marks/index.js +11 -0
- package/dist/foundation/marks/normalization.d.ts +2 -0
- package/dist/foundation/marks/normalization.js +49 -0
- package/dist/foundation/marks/plugin.d.ts +14 -0
- package/dist/foundation/marks/plugin.js +63 -0
- package/dist/foundation/marks/resolveMarkRun.d.ts +8 -0
- package/dist/foundation/marks/resolveMarkRun.js +47 -0
- package/dist/foundation/marks/schema.d.ts +167 -0
- package/dist/foundation/marks/schema.js +50 -0
- package/dist/foundation/marks/stored.d.ts +2 -0
- package/dist/foundation/marks/stored.js +31 -0
- package/dist/foundation/marks/types.d.ts +40 -0
- package/dist/foundation/marks/types.js +1 -0
- package/dist/foundation/modelDom.d.ts +38 -0
- package/dist/foundation/modelDom.js +327 -0
- package/dist/foundation/normalization.d.ts +22 -0
- package/dist/foundation/normalization.js +132 -0
- package/dist/foundation/operations.d.ts +28 -0
- package/dist/foundation/operations.js +568 -0
- package/dist/foundation/plugin/builtins.d.ts +10 -0
- package/dist/foundation/plugin/builtins.js +27 -0
- package/dist/foundation/plugin/dispatch.d.ts +33 -0
- package/dist/foundation/plugin/dispatch.js +34 -0
- package/dist/foundation/plugin/index.d.ts +4 -0
- package/dist/foundation/plugin/index.js +4 -0
- package/dist/foundation/plugin/registry.d.ts +36 -0
- package/dist/foundation/plugin/registry.js +115 -0
- package/dist/foundation/plugin/types.d.ts +141 -0
- package/dist/foundation/plugin/types.js +13 -0
- package/dist/foundation/positions.d.ts +20 -0
- package/dist/foundation/positions.js +121 -0
- package/dist/foundation/schema.d.ts +57 -0
- package/dist/foundation/schema.js +371 -0
- package/dist/foundation/schemaBuilder.d.ts +41 -0
- package/dist/foundation/schemaBuilder.js +96 -0
- package/dist/foundation/scope/index.d.ts +2 -0
- package/dist/foundation/scope/index.js +2 -0
- package/dist/foundation/scope/resolveScope.d.ts +13 -0
- package/dist/foundation/scope/resolveScope.js +860 -0
- package/dist/foundation/scope/types.d.ts +122 -0
- package/dist/foundation/scope/types.js +1 -0
- package/dist/foundation/security/urlPolicy.d.ts +21 -0
- package/dist/foundation/security/urlPolicy.js +75 -0
- package/dist/foundation/structural/contentTarget.d.ts +21 -0
- package/dist/foundation/structural/contentTarget.js +51 -0
- package/dist/foundation/structural/index.d.ts +2 -0
- package/dist/foundation/structural/index.js +2 -0
- package/dist/foundation/structural/move.d.ts +7 -0
- package/dist/foundation/structural/move.js +35 -0
- package/dist/foundation/suggestions/index.d.ts +3 -0
- package/dist/foundation/suggestions/index.js +3 -0
- package/dist/foundation/suggestions/inline.d.ts +64 -0
- package/dist/foundation/suggestions/inline.js +116 -0
- package/dist/foundation/suggestions/structural.d.ts +40 -0
- package/dist/foundation/suggestions/structural.js +47 -0
- package/dist/foundation/suggestions/types.d.ts +19 -0
- package/dist/foundation/suggestions/types.js +1 -0
- package/dist/foundation/surface/index.d.ts +3 -0
- package/dist/foundation/surface/index.js +3 -0
- package/dist/foundation/surface/input.d.ts +127 -0
- package/dist/foundation/surface/input.js +1761 -0
- package/dist/foundation/surface/renderer.d.ts +84 -0
- package/dist/foundation/surface/renderer.js +974 -0
- package/dist/foundation/surface/types.d.ts +48 -0
- package/dist/foundation/surface/types.js +1 -0
- package/dist/foundation/table/commands.d.ts +31 -0
- package/dist/foundation/table/commands.js +470 -0
- package/dist/foundation/table/formats.d.ts +27 -0
- package/dist/foundation/table/formats.js +40 -0
- package/dist/foundation/table/grid.d.ts +8 -0
- package/dist/foundation/table/grid.js +178 -0
- package/dist/foundation/table/index.d.ts +6 -0
- package/dist/foundation/table/index.js +6 -0
- package/dist/foundation/table/plugin.d.ts +23 -0
- package/dist/foundation/table/plugin.js +119 -0
- package/dist/foundation/table/schema.d.ts +2 -0
- package/dist/foundation/table/schema.js +38 -0
- package/dist/foundation/table/selection.d.ts +7 -0
- package/dist/foundation/table/selection.js +35 -0
- package/dist/foundation/table/types.d.ts +109 -0
- package/dist/foundation/table/types.js +1 -0
- package/dist/foundation/transactions.d.ts +41 -0
- package/dist/foundation/transactions.js +90 -0
- package/dist/foundation/types.d.ts +261 -0
- package/dist/foundation/types.js +1 -0
- package/dist/foundation/versioning/diffVersions.d.ts +5 -0
- package/dist/foundation/versioning/diffVersions.js +3 -0
- package/dist/foundation/versioning/index.d.ts +2 -0
- package/dist/foundation/versioning/index.js +2 -0
- package/dist/foundation/versioning/types.d.ts +17 -0
- package/dist/foundation/versioning/types.js +1 -0
- package/dist/history.d.ts +20 -0
- package/dist/history.js +165 -0
- package/dist/html/compatibility.js +7 -1
- package/dist/index.d.ts +1 -14
- package/dist/index.js +9 -14
- package/dist/legacy/index.d.ts +45 -0
- package/dist/legacy/index.js +45 -0
- package/dist/legacyCommands/alignment.d.ts +20 -0
- package/dist/{commands → legacyCommands}/alignment.js +17 -0
- package/dist/legacyCommands/blocks.d.ts +14 -0
- package/dist/legacyCommands/blocks.js +105 -0
- package/dist/legacyCommands/checklist.d.ts +11 -0
- package/dist/legacyCommands/checklist.js +45 -0
- package/dist/legacyCommands/code.d.ts +15 -0
- package/dist/{commands → legacyCommands}/code.js +25 -1
- package/dist/legacyCommands/formula.d.ts +7 -0
- package/dist/legacyCommands/formula.js +67 -0
- package/dist/legacyCommands/inlineAtoms.d.ts +7 -0
- package/dist/legacyCommands/inlineAtoms.js +58 -0
- package/dist/legacyCommands/list.d.ts +38 -0
- package/dist/legacyCommands/list.js +477 -0
- package/dist/{commands → legacyCommands}/marks.d.ts +3 -0
- package/dist/{commands → legacyCommands}/marks.js +28 -11
- package/dist/legacyCommands/media.d.ts +31 -0
- package/dist/legacyCommands/media.js +205 -0
- package/dist/legacyCommands/move.d.ts +14 -0
- package/dist/legacyCommands/move.js +91 -0
- package/dist/legacyCommands/table.d.ts +91 -0
- package/dist/legacyCommands/table.js +547 -0
- package/dist/listPresets.d.ts +12 -0
- package/dist/listPresets.js +6 -0
- package/dist/listScope.d.ts +27 -0
- package/dist/listScope.js +104 -0
- package/dist/markdown/compatibility.d.ts +2 -0
- package/dist/markdown/compatibility.js +94 -2
- package/dist/marks.d.ts +5 -5
- package/dist/model.d.ts +65 -9
- package/dist/plugin.d.ts +15 -0
- package/dist/plugin.js +31 -0
- package/dist/plugins/alignment.d.ts +3 -0
- package/dist/plugins/alignment.js +6 -0
- package/dist/plugins/basicFormatting.d.ts +9 -0
- package/dist/plugins/basicFormatting.js +24 -0
- package/dist/plugins/blockType.d.ts +3 -0
- package/dist/plugins/blockType.js +6 -0
- package/dist/plugins/blockquote.d.ts +3 -0
- package/dist/plugins/blockquote.js +6 -0
- package/dist/plugins/checklist.d.ts +3 -0
- package/dist/plugins/checklist.js +10 -0
- package/dist/plugins/codeBlock.d.ts +3 -0
- package/dist/plugins/codeBlock.js +6 -0
- package/dist/plugins/formula.d.ts +3 -0
- package/dist/plugins/formula.js +9 -0
- package/dist/plugins/list.d.ts +3 -0
- package/dist/plugins/list.js +10 -0
- package/dist/plugins/media.d.ts +3 -0
- package/dist/plugins/media.js +12 -0
- package/dist/plugins/move.d.ts +3 -0
- package/dist/plugins/move.js +9 -0
- package/dist/plugins/table.d.ts +3 -0
- package/dist/plugins/table.js +22 -0
- package/dist/preset.d.ts +12 -0
- package/dist/preset.js +40 -0
- package/dist/schema.d.ts +22 -0
- package/dist/schema.js +333 -0
- package/dist/security/urlPolicy.d.ts +1 -17
- package/dist/security/urlPolicy.js +3 -69
- package/dist/selection.d.ts +1 -1
- package/dist/selectionMapping.d.ts +4 -0
- package/dist/selectionMapping.js +222 -0
- package/dist/table.d.ts +3 -3
- package/dist/transaction.d.ts +35 -12
- package/dist/transaction.js +174 -28
- package/dist/tree.d.ts +8 -2
- package/dist/tree.js +44 -0
- package/package.json +27 -8
- package/dist/commands/alignment.d.ts +0 -18
- package/dist/commands/code.d.ts +0 -13
- package/dist/commands/list.d.ts +0 -20
- package/dist/commands/list.js +0 -52
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.2
|
|
4
|
+
|
|
5
|
+
- Add an opt-in `renderFormulaHtml` option to `serializeCanonicalListHtml` that bakes real KaTeX-rendered HTML into exported formula elements, instead of leaving an empty placeholder — for consumers (e.g. a read-only preview) that display this HTML directly without also running KaTeX against it themselves. Off by default; no change for existing consumers.
|
|
6
|
+
|
|
7
|
+
## 1.0.0-beta.1
|
|
8
|
+
|
|
9
|
+
**BREAKING CHANGE.** This is the first release built on the canonical, schema-driven document model. Every published version through 0.2.1 was built on the older discriminated-union model; none of the work below has shipped before.
|
|
10
|
+
|
|
11
|
+
- Introduce the canonical document model and editing engine (`smartrte-core/foundation`): schema-driven nodes with stable IDs, a scope/position API, structural history, and dedicated engines for lists, marks, blocks, tables, and atoms (images, formulas, media).
|
|
12
|
+
- Add a clipboard pipeline with source-aware normalization (Word, Google Docs, spreadsheets, Markdown, native) and a sanitize-first security boundary.
|
|
13
|
+
- Add DOCX, PDF, and Markdown format codecs with an explicit, tested `FeatureFormatCodec` fidelity contract (`full`/`semantic`/`lossy`/`unsupported`) per feature and format — no fidelity claim ships without a passing round-trip fixture.
|
|
14
|
+
- Add live KaTeX rendering for formula atoms (real HTML + MathML output, `trust:false`, `strict:"error"`), replacing plain-text LaTeX source display.
|
|
15
|
+
- Make the package genuinely framework-agnostic: no DOM-library dependency in the format codecs (parse5/`@xmldom/xmldom` replace real-DOM usage), zero React imports anywhere in `smartrte-core`.
|
|
16
|
+
- **Breaking:** the package root (`.`) no longer re-exports the legacy discriminated-union model, its plugin system, or the HTML/Markdown compatibility layers. That surface is unchanged and still fully available at `smartrte-core/legacy`; only the accidental root-level duplication was removed.
|
|
17
|
+
- **Breaking:** `smartrte-core/foundation` now exports `FOUNDATION_SMART_LIST_PRESETS` and related list-preset helpers (previously unreachable from the package's public exports).
|
|
18
|
+
|
|
3
19
|
## 0.2.1
|
|
4
20
|
|
|
5
21
|
- Keep superscript and subscript mutually exclusive when converting selected text.
|
package/README.md
CHANGED
|
@@ -3,3 +3,26 @@
|
|
|
3
3
|
Framework-agnostic document model, selection, transaction, history, and
|
|
4
4
|
normalization primitives. This package is not yet wired into the published
|
|
5
5
|
React editor; it establishes the compatibility-safe migration boundary.
|
|
6
|
+
|
|
7
|
+
## Foundation contract
|
|
8
|
+
|
|
9
|
+
The frozen Phase 1/C0 kernel is available from the dedicated subpath:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
createFoundationEditor,
|
|
14
|
+
foundationSchema,
|
|
15
|
+
resolveScope,
|
|
16
|
+
type ResolvedPos,
|
|
17
|
+
type SmartTransaction,
|
|
18
|
+
} from "smartrte-core/foundation";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The package root remains the compatibility API for the current React adapter.
|
|
22
|
+
New kernel and Phase 2 resolver work must use `smartrte-core/foundation`; do not
|
|
23
|
+
extend the legacy raw-path contracts. `pnpm run lint:foundation-boundary`
|
|
24
|
+
enforces that boundary. Root promotion is an explicit Phase 3 list-migration
|
|
25
|
+
exit criterion. See `docs/FOUNDATION_CONTRACT.md` in the repository for the
|
|
26
|
+
locked decisions and `docs/SEMANTIC_SELECTION_CONTRACT.md` for Phase 2.
|
|
27
|
+
The persistent apply, incremental index, renderer, and browser-input contracts
|
|
28
|
+
are recorded in `docs/CANONICAL_EDITING_SURFACE.md`.
|
package/dist/command.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
export interface
|
|
5
|
-
document:
|
|
6
|
-
selection:
|
|
1
|
+
import type { LegacySmartDocument } from "./model.js";
|
|
2
|
+
import type { LegacySmartSelection } from "./selection.js";
|
|
3
|
+
import type { LegacySmartTransaction } from "./transaction.js";
|
|
4
|
+
export interface LegacyCommandContext {
|
|
5
|
+
document: LegacySmartDocument;
|
|
6
|
+
selection: LegacySmartSelection;
|
|
7
7
|
now?: () => number;
|
|
8
8
|
}
|
|
9
9
|
export interface SmartCommand<Input = unknown> {
|
|
10
10
|
id: string;
|
|
11
|
-
isEnabled(context:
|
|
12
|
-
execute(context:
|
|
11
|
+
isEnabled(context: LegacyCommandContext, input?: Input): boolean;
|
|
12
|
+
execute(context: LegacyCommandContext, input?: Input): LegacySmartTransaction;
|
|
13
13
|
}
|
package/dist/editor.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type SmartHistoryState } from "./history.js";
|
|
2
|
+
import { type SmartRtePlugin } from "./plugin.js";
|
|
3
|
+
import { type SmartEditorState, type LegacySmartTransaction } from "./transaction.js";
|
|
4
|
+
export interface CreateSmartEditorOptions {
|
|
5
|
+
state: SmartEditorState;
|
|
6
|
+
plugins?: SmartRtePlugin[];
|
|
7
|
+
historyLimit?: number;
|
|
8
|
+
readOnly?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type SmartEditorListener = (state: SmartEditorState, transaction: LegacySmartTransaction) => void;
|
|
11
|
+
export declare class SmartEditor {
|
|
12
|
+
private currentState;
|
|
13
|
+
private currentHistory;
|
|
14
|
+
private readonly plugins;
|
|
15
|
+
private readonly schema;
|
|
16
|
+
private readonly commands;
|
|
17
|
+
private readonly listeners;
|
|
18
|
+
private readOnly;
|
|
19
|
+
constructor(options: CreateSmartEditorOptions);
|
|
20
|
+
get state(): SmartEditorState;
|
|
21
|
+
get history(): SmartHistoryState;
|
|
22
|
+
get pluginIds(): string[];
|
|
23
|
+
setReadOnly(readOnly: boolean): void;
|
|
24
|
+
canExecute<Input>(id: string, input?: Input): boolean;
|
|
25
|
+
execute<Input>(id: string, input?: Input): boolean;
|
|
26
|
+
dispatch(transaction: LegacySmartTransaction): boolean;
|
|
27
|
+
undo(): boolean;
|
|
28
|
+
redo(): boolean;
|
|
29
|
+
subscribe(listener: SmartEditorListener): () => void;
|
|
30
|
+
private withPluginNormalization;
|
|
31
|
+
}
|
|
32
|
+
export declare const createSmartEditor: (options: CreateSmartEditorOptions) => SmartEditor;
|
package/dist/editor.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { applyTransactionWithHistory, createHistoryState, redoHistory, undoHistory, } from "./history.js";
|
|
2
|
+
import { createSmartSchema, normalizeSmartDocument, validateSmartDocument, } from "./schema.js";
|
|
3
|
+
import { orderPlugins } from "./plugin.js";
|
|
4
|
+
import { applyTransaction } from "./transaction.js";
|
|
5
|
+
const sameDocument = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
6
|
+
export class SmartEditor {
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.commands = new Map();
|
|
9
|
+
this.listeners = new Set();
|
|
10
|
+
this.plugins = orderPlugins(options.plugins || []);
|
|
11
|
+
this.schema = createSmartSchema(this.plugins
|
|
12
|
+
.filter((plugin) => plugin.schema)
|
|
13
|
+
.map((plugin) => ({ id: plugin.id, ...plugin.schema })));
|
|
14
|
+
const document = normalizeSmartDocument(options.state.document, this.schema);
|
|
15
|
+
const validation = validateSmartDocument(document, this.schema);
|
|
16
|
+
if (!validation.valid)
|
|
17
|
+
throw new Error(`Initial document is invalid: ${validation.issues[0]?.message}`);
|
|
18
|
+
this.currentState = { document, selection: options.state.selection };
|
|
19
|
+
this.currentHistory = createHistoryState(options.historyLimit);
|
|
20
|
+
this.readOnly = options.readOnly ?? false;
|
|
21
|
+
this.plugins.forEach((plugin) => {
|
|
22
|
+
Object.entries(plugin.commands || {}).forEach(([id, command]) => {
|
|
23
|
+
if (this.commands.has(id))
|
|
24
|
+
throw new Error(`Duplicate command id "${id}".`);
|
|
25
|
+
this.commands.set(id, command);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
get state() {
|
|
30
|
+
return this.currentState;
|
|
31
|
+
}
|
|
32
|
+
get history() {
|
|
33
|
+
return this.currentHistory;
|
|
34
|
+
}
|
|
35
|
+
get pluginIds() {
|
|
36
|
+
return this.plugins.map((plugin) => plugin.id);
|
|
37
|
+
}
|
|
38
|
+
setReadOnly(readOnly) {
|
|
39
|
+
this.readOnly = readOnly;
|
|
40
|
+
}
|
|
41
|
+
canExecute(id, input) {
|
|
42
|
+
if (this.readOnly)
|
|
43
|
+
return false;
|
|
44
|
+
const command = this.commands.get(id);
|
|
45
|
+
return Boolean(command?.isEnabled(this.currentState, input));
|
|
46
|
+
}
|
|
47
|
+
execute(id, input) {
|
|
48
|
+
if (!this.canExecute(id, input))
|
|
49
|
+
return false;
|
|
50
|
+
const command = this.commands.get(id);
|
|
51
|
+
return this.dispatch(command.execute(this.currentState, input));
|
|
52
|
+
}
|
|
53
|
+
dispatch(transaction) {
|
|
54
|
+
if (this.readOnly && transaction.source === "user")
|
|
55
|
+
return false;
|
|
56
|
+
const prepared = this.withPluginNormalization(transaction);
|
|
57
|
+
const result = applyTransactionWithHistory(this.currentState, this.currentHistory, prepared);
|
|
58
|
+
this.currentState = result.state;
|
|
59
|
+
this.currentHistory = result.history;
|
|
60
|
+
this.plugins.forEach((plugin) => plugin.onTransaction?.(prepared));
|
|
61
|
+
this.listeners.forEach((listener) => listener(this.currentState, prepared));
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
undo() {
|
|
65
|
+
if (this.readOnly)
|
|
66
|
+
return false;
|
|
67
|
+
const result = undoHistory(this.currentState, this.currentHistory);
|
|
68
|
+
if (!result.applied)
|
|
69
|
+
return false;
|
|
70
|
+
this.currentState = result.state;
|
|
71
|
+
this.currentHistory = result.history;
|
|
72
|
+
const transaction = result.history.redo[result.history.redo.length - 1]?.inverse;
|
|
73
|
+
if (transaction)
|
|
74
|
+
this.listeners.forEach((listener) => listener(this.currentState, transaction));
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
redo() {
|
|
78
|
+
if (this.readOnly)
|
|
79
|
+
return false;
|
|
80
|
+
const result = redoHistory(this.currentState, this.currentHistory);
|
|
81
|
+
if (!result.applied)
|
|
82
|
+
return false;
|
|
83
|
+
this.currentState = result.state;
|
|
84
|
+
this.currentHistory = result.history;
|
|
85
|
+
const transaction = result.history.undo[result.history.undo.length - 1]?.forward;
|
|
86
|
+
if (transaction)
|
|
87
|
+
this.listeners.forEach((listener) => listener(this.currentState, transaction));
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
subscribe(listener) {
|
|
91
|
+
this.listeners.add(listener);
|
|
92
|
+
return () => this.listeners.delete(listener);
|
|
93
|
+
}
|
|
94
|
+
withPluginNormalization(transaction) {
|
|
95
|
+
const base = applyTransaction(this.currentState, transaction);
|
|
96
|
+
let document = base.document;
|
|
97
|
+
for (let pass = 0; pass < 10; pass += 1) {
|
|
98
|
+
const beforePass = document;
|
|
99
|
+
document = normalizeSmartDocument(document, this.schema);
|
|
100
|
+
this.plugins.forEach((plugin) => {
|
|
101
|
+
(plugin.normalizers || []).forEach((normalizer) => {
|
|
102
|
+
document = normalizeSmartDocument(normalizer(document), this.schema);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
if (sameDocument(document, beforePass))
|
|
106
|
+
break;
|
|
107
|
+
if (pass === 9)
|
|
108
|
+
throw new Error("Plugin normalization did not reach a fixed point.");
|
|
109
|
+
}
|
|
110
|
+
const validation = validateSmartDocument(document, this.schema);
|
|
111
|
+
if (!validation.valid)
|
|
112
|
+
throw new Error(`Plugin normalization produced an invalid document: ${validation.issues[0]?.message}`);
|
|
113
|
+
if (sameDocument(document, base.document))
|
|
114
|
+
return transaction;
|
|
115
|
+
return {
|
|
116
|
+
...transaction,
|
|
117
|
+
operations: [...transaction.operations, { type: "replaceNode", path: [], node: document }],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export const createSmartEditor = (options) => new SmartEditor(options);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { SmartDocument, SmartOperation, SmartRange } from "../types.js";
|
|
2
|
+
import type { PositionLookup } from "../scope/types.js";
|
|
3
|
+
import type { AnnotationRange, MergeOrphanPolicy } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves an AnnotationRange against the current document. For every
|
|
6
|
+
* operation type except splitNode/mergeNode, node identity survives
|
|
7
|
+
* unchanged (move: the id follows the node; insert/remove/text/marks: never
|
|
8
|
+
* touch node ids; setNodeType: id explicitly preserved), so this alone is
|
|
9
|
+
* enough to track a range through most edits - PositionLookup's `positionOf`
|
|
10
|
+
* resolves it fresh each call. Returns null once an anchor id no longer
|
|
11
|
+
* exists (removed, or retired by a merge with no rebase applied).
|
|
12
|
+
*/
|
|
13
|
+
export declare const resolveAnnotationRange: (range: AnnotationRange, positions: PositionLookup) => SmartRange | null;
|
|
14
|
+
/**
|
|
15
|
+
* Rebases an AnnotationRange through a single operation. Every operation
|
|
16
|
+
* type other than split/merge/an-explicitly-marked-merge-shaped-removeNode-
|
|
17
|
+
* or-replaceNode is a no-op here - node identity already survives them, so
|
|
18
|
+
* the range's stored ids stay valid and `resolveAnnotationRange` picks up
|
|
19
|
+
* the new position on its own next call. `positions` must resolve against
|
|
20
|
+
* the document as it existed immediately before `operation` is applied.
|
|
21
|
+
*/
|
|
22
|
+
export declare const rebaseAnnotationRange: (range: AnnotationRange, operation: SmartOperation, positions: PositionLookup, onOrphan: MergeOrphanPolicy) => AnnotationRange | null;
|
|
23
|
+
/**
|
|
24
|
+
* Folds `rebaseAnnotationRange` over a sequence of operations, e.g. every
|
|
25
|
+
* operation in one transaction, in order. Each step's `positions` must
|
|
26
|
+
* resolve against the document as it existed immediately before that step's
|
|
27
|
+
* operation - the caller (which is already applying the operations to
|
|
28
|
+
* advance the document) is in the natural position to supply this.
|
|
29
|
+
*/
|
|
30
|
+
export declare const rebaseAnnotationRangeThroughOperations: (range: AnnotationRange, steps: readonly {
|
|
31
|
+
readonly operation: SmartOperation;
|
|
32
|
+
readonly positions: PositionLookup;
|
|
33
|
+
}[], onOrphan: MergeOrphanPolicy) => AnnotationRange | null;
|
|
34
|
+
/**
|
|
35
|
+
* Reference `MergeOrphanPolicy`: snap to the surviving node rather than
|
|
36
|
+
* drop (Phase 12a's decision - a comment on content that got merged into
|
|
37
|
+
* something else is still relevant to that content's new home; silently
|
|
38
|
+
* vanishing it loses data with no recovery path). Only applicable to a
|
|
39
|
+
* real `mergeNode` operation (`input.ts`'s cross-block Backspace/Delete
|
|
40
|
+
* text-merge) - unlike `mergedInto`/`retiredInto` above, `mergeNode`'s own
|
|
41
|
+
* shape has no explicit survivor field, so deriving it needs the actual
|
|
42
|
+
* document tree (the sibling immediately preceding the merge point,
|
|
43
|
+
* matching how `input.ts`'s `queueRangeDeletion` constructs the
|
|
44
|
+
* operation: `retiredId` is the right/consumed block, the left/absorbing
|
|
45
|
+
* block sits one offset earlier at the same parent). `positions` alone
|
|
46
|
+
* (an id-keyed lookup) can't answer "what's at this path" in the reverse
|
|
47
|
+
* direction, so this needs `document` explicitly, unlike every other
|
|
48
|
+
* function in this file.
|
|
49
|
+
*
|
|
50
|
+
* Table-cell-merge and list-item-merge don't need this - they always
|
|
51
|
+
* populate `mergedInto`/`retiredInto` themselves, so `rebaseAnnotationRange`
|
|
52
|
+
* snaps them unconditionally with no caller policy involved at all.
|
|
53
|
+
*/
|
|
54
|
+
export declare const defaultMergeOrphanPolicy: (document: SmartDocument) => MergeOrphanPolicy;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { isTextNode } from "../identity.js";
|
|
2
|
+
import { nodeAtPath } from "../positions.js";
|
|
3
|
+
const samePath = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
|
|
4
|
+
const resolveEndpoint = (id, offset, side, positions) => {
|
|
5
|
+
if (offset === undefined) {
|
|
6
|
+
const outer = positions.rangeOf(id);
|
|
7
|
+
return outer ? outer[side] : null;
|
|
8
|
+
}
|
|
9
|
+
const resolved = positions.positionOf(id);
|
|
10
|
+
if (!resolved)
|
|
11
|
+
return null;
|
|
12
|
+
return { path: [...resolved.pos.path, resolved.pos.offset], offset };
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Resolves an AnnotationRange against the current document. For every
|
|
16
|
+
* operation type except splitNode/mergeNode, node identity survives
|
|
17
|
+
* unchanged (move: the id follows the node; insert/remove/text/marks: never
|
|
18
|
+
* touch node ids; setNodeType: id explicitly preserved), so this alone is
|
|
19
|
+
* enough to track a range through most edits - PositionLookup's `positionOf`
|
|
20
|
+
* resolves it fresh each call. Returns null once an anchor id no longer
|
|
21
|
+
* exists (removed, or retired by a merge with no rebase applied).
|
|
22
|
+
*/
|
|
23
|
+
export const resolveAnnotationRange = (range, positions) => {
|
|
24
|
+
const from = resolveEndpoint(range.startId, range.startOffset, "from", positions);
|
|
25
|
+
const to = resolveEndpoint(range.endId, range.endOffset, "to", positions);
|
|
26
|
+
return from && to ? { from, to } : null;
|
|
27
|
+
};
|
|
28
|
+
const splitTargetPath = (operation) => operation.pos.path.slice(0, operation.pos.path.length - operation.depth);
|
|
29
|
+
/**
|
|
30
|
+
* Split keeps the pre-split id on the left half and mints `newId` for the
|
|
31
|
+
* right half. An endpoint whose offset lands at or past the split point
|
|
32
|
+
* moved into the right half - its id must be rewritten, or it would keep
|
|
33
|
+
* resolving (via `positionOf`) to the left half's now-unrelated content,
|
|
34
|
+
* silently pointing at the wrong node instead of failing loudly. An
|
|
35
|
+
* endpoint anchored to a different node entirely, or one whose offset is
|
|
36
|
+
* undefined (whole-node anchor, not yet split-aware), passes through
|
|
37
|
+
* unchanged.
|
|
38
|
+
*/
|
|
39
|
+
const rebaseEndpointThroughSplit = (id, offset, operation, positions) => {
|
|
40
|
+
if (offset === undefined)
|
|
41
|
+
return { id, offset };
|
|
42
|
+
const resolved = positions.positionOf(id);
|
|
43
|
+
if (!resolved)
|
|
44
|
+
return { id, offset };
|
|
45
|
+
const path = [...resolved.pos.path, resolved.pos.offset];
|
|
46
|
+
if (!samePath(path, splitTargetPath(operation)))
|
|
47
|
+
return { id, offset };
|
|
48
|
+
if (offset < operation.pos.offset)
|
|
49
|
+
return { id, offset };
|
|
50
|
+
return { id: operation.newId, offset: offset - operation.pos.offset };
|
|
51
|
+
};
|
|
52
|
+
const rebaseThroughSplit = (range, operation, positions) => {
|
|
53
|
+
const start = rebaseEndpointThroughSplit(range.startId, range.startOffset, operation, positions);
|
|
54
|
+
const end = rebaseEndpointThroughSplit(range.endId, range.endOffset, operation, positions);
|
|
55
|
+
return { startId: start.id, startOffset: start.offset, endId: end.id, endOffset: end.offset };
|
|
56
|
+
};
|
|
57
|
+
const rebaseThroughMerge = (range, operation, positions, onOrphan) => {
|
|
58
|
+
if (range.startId !== operation.retiredId && range.endId !== operation.retiredId)
|
|
59
|
+
return range;
|
|
60
|
+
return onOrphan(range, operation, positions);
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* An endpoint anchored to a retired id snaps to its survivor's whole-node
|
|
64
|
+
* span (offset cleared, not remapped) - the survivor is a different node
|
|
65
|
+
* whose content was assembled by concatenating multiple sources in an
|
|
66
|
+
* order this generic mechanism has no way to know (e.g. `mergeItems`'
|
|
67
|
+
* `mergeOwners` combines two paragraphs' text in an order that depends on
|
|
68
|
+
* merge direction), so a stale offset into the retired node's own text
|
|
69
|
+
* cannot be reliably remapped into the merged text - a wrong-but-
|
|
70
|
+
* plausible-looking offset would be worse than an honest whole-node
|
|
71
|
+
* anchor. Unlike the mergeNode case above, there is no caller-supplied
|
|
72
|
+
* policy here: `mergedInto`/`retiredInto` are opt-in fields a command
|
|
73
|
+
* populates only when it knows exactly where the content went, so
|
|
74
|
+
* snapping is the unambiguous, mechanical consequence of the field's
|
|
75
|
+
* presence, not a product decision requiring caller input - the actual
|
|
76
|
+
* product decision already happened at the command layer, by choosing to
|
|
77
|
+
* populate the field at all instead of leaving the operation as a plain,
|
|
78
|
+
* policy-free removeNode/replaceNode.
|
|
79
|
+
*/
|
|
80
|
+
const snapEndpoint = (id, offset, retiredId, survivorId) => id === retiredId ? { id: survivorId } : { id, offset };
|
|
81
|
+
// Preserves reference identity when neither endpoint actually matches
|
|
82
|
+
// retiredId - a consumer rebasing many ranges through the same operation
|
|
83
|
+
// (see comments/rebase.ts) relies on `===` to cheaply tell which ones
|
|
84
|
+
// actually changed, e.g. to avoid re-persisting or re-rendering untouched
|
|
85
|
+
// threads.
|
|
86
|
+
const rebaseThroughMergedRemove = (range, retiredId, survivorId) => {
|
|
87
|
+
if (range.startId !== retiredId && range.endId !== retiredId)
|
|
88
|
+
return range;
|
|
89
|
+
const start = snapEndpoint(range.startId, range.startOffset, retiredId, survivorId);
|
|
90
|
+
const end = snapEndpoint(range.endId, range.endOffset, retiredId, survivorId);
|
|
91
|
+
return { startId: start.id, startOffset: start.offset, endId: end.id, endOffset: end.offset };
|
|
92
|
+
};
|
|
93
|
+
const rebaseThroughRetiredReplace = (range, retirements) => retirements.reduce((current, { retiredId, survivorId }) => rebaseThroughMergedRemove(current, retiredId, survivorId), range);
|
|
94
|
+
/**
|
|
95
|
+
* Rebases an AnnotationRange through a single operation. Every operation
|
|
96
|
+
* type other than split/merge/an-explicitly-marked-merge-shaped-removeNode-
|
|
97
|
+
* or-replaceNode is a no-op here - node identity already survives them, so
|
|
98
|
+
* the range's stored ids stay valid and `resolveAnnotationRange` picks up
|
|
99
|
+
* the new position on its own next call. `positions` must resolve against
|
|
100
|
+
* the document as it existed immediately before `operation` is applied.
|
|
101
|
+
*/
|
|
102
|
+
export const rebaseAnnotationRange = (range, operation, positions, onOrphan) => {
|
|
103
|
+
if (operation.type === "splitNode")
|
|
104
|
+
return rebaseThroughSplit(range, operation, positions);
|
|
105
|
+
if (operation.type === "mergeNode")
|
|
106
|
+
return rebaseThroughMerge(range, operation, positions, onOrphan);
|
|
107
|
+
if (operation.type === "removeNode" && operation.mergedInto && !isTextNode(operation.node)) {
|
|
108
|
+
return rebaseThroughMergedRemove(range, operation.node.id, operation.mergedInto);
|
|
109
|
+
}
|
|
110
|
+
if (operation.type === "replaceNode" && operation.retiredInto?.length)
|
|
111
|
+
return rebaseThroughRetiredReplace(range, operation.retiredInto);
|
|
112
|
+
return range;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Folds `rebaseAnnotationRange` over a sequence of operations, e.g. every
|
|
116
|
+
* operation in one transaction, in order. Each step's `positions` must
|
|
117
|
+
* resolve against the document as it existed immediately before that step's
|
|
118
|
+
* operation - the caller (which is already applying the operations to
|
|
119
|
+
* advance the document) is in the natural position to supply this.
|
|
120
|
+
*/
|
|
121
|
+
export const rebaseAnnotationRangeThroughOperations = (range, steps, onOrphan) => {
|
|
122
|
+
let current = range;
|
|
123
|
+
for (const step of steps) {
|
|
124
|
+
if (!current)
|
|
125
|
+
return null;
|
|
126
|
+
current = rebaseAnnotationRange(current, step.operation, step.positions, onOrphan);
|
|
127
|
+
}
|
|
128
|
+
return current;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Reference `MergeOrphanPolicy`: snap to the surviving node rather than
|
|
132
|
+
* drop (Phase 12a's decision - a comment on content that got merged into
|
|
133
|
+
* something else is still relevant to that content's new home; silently
|
|
134
|
+
* vanishing it loses data with no recovery path). Only applicable to a
|
|
135
|
+
* real `mergeNode` operation (`input.ts`'s cross-block Backspace/Delete
|
|
136
|
+
* text-merge) - unlike `mergedInto`/`retiredInto` above, `mergeNode`'s own
|
|
137
|
+
* shape has no explicit survivor field, so deriving it needs the actual
|
|
138
|
+
* document tree (the sibling immediately preceding the merge point,
|
|
139
|
+
* matching how `input.ts`'s `queueRangeDeletion` constructs the
|
|
140
|
+
* operation: `retiredId` is the right/consumed block, the left/absorbing
|
|
141
|
+
* block sits one offset earlier at the same parent). `positions` alone
|
|
142
|
+
* (an id-keyed lookup) can't answer "what's at this path" in the reverse
|
|
143
|
+
* direction, so this needs `document` explicitly, unlike every other
|
|
144
|
+
* function in this file.
|
|
145
|
+
*
|
|
146
|
+
* Table-cell-merge and list-item-merge don't need this - they always
|
|
147
|
+
* populate `mergedInto`/`retiredInto` themselves, so `rebaseAnnotationRange`
|
|
148
|
+
* snaps them unconditionally with no caller policy involved at all.
|
|
149
|
+
*/
|
|
150
|
+
export const defaultMergeOrphanPolicy = (document) => (range, operation) => {
|
|
151
|
+
const parent = nodeAtPath(document, operation.pos.path);
|
|
152
|
+
if (!parent || isTextNode(parent) || !parent.children)
|
|
153
|
+
return null;
|
|
154
|
+
const survivor = parent.children[operation.pos.offset - 1];
|
|
155
|
+
if (!survivor || isTextNode(survivor))
|
|
156
|
+
return null;
|
|
157
|
+
return rebaseThroughMergedRemove(range, operation.retiredId, survivor.id);
|
|
158
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SmartOperation } from "../types.js";
|
|
2
|
+
import type { PositionLookup } from "../scope/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* A range anchored to node ids rather than a path/offset, so it survives
|
|
5
|
+
* structural edits elsewhere in the document without being carried through
|
|
6
|
+
* every transaction. Resolve it on demand via `resolveAnnotationRange`.
|
|
7
|
+
*
|
|
8
|
+
* An absent offset anchors to the whole node (its outer span) rather than a
|
|
9
|
+
* text position inside it. There is deliberately no `kind` discriminant yet
|
|
10
|
+
* (text range vs. whole-node anchor) - that is left for whichever phase
|
|
11
|
+
* first needs to distinguish them, so this primitive does not have to be
|
|
12
|
+
* retrofitted once real consumers exist.
|
|
13
|
+
*/
|
|
14
|
+
export interface AnnotationRange {
|
|
15
|
+
readonly startId: string;
|
|
16
|
+
readonly startOffset?: number;
|
|
17
|
+
readonly endId: string;
|
|
18
|
+
readonly endOffset?: number;
|
|
19
|
+
}
|
|
20
|
+
export type SplitNodeOperation = Extract<SmartOperation, {
|
|
21
|
+
type: "splitNode";
|
|
22
|
+
}>;
|
|
23
|
+
export type MergeNodeOperation = Extract<SmartOperation, {
|
|
24
|
+
type: "mergeNode";
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Called when a merge retires a node id an AnnotationRange endpoint was
|
|
28
|
+
* anchored to. foundation/ has no opinion on whether the range should be
|
|
29
|
+
* dropped, snapped into the surviving node, or something else - comments and
|
|
30
|
+
* suggestions plausibly want different answers - so that decision is left
|
|
31
|
+
* entirely to the caller rather than baked into the primitive.
|
|
32
|
+
*/
|
|
33
|
+
export type MergeOrphanPolicy = (range: AnnotationRange, operation: MergeNodeOperation, positions: PositionLookup) => AnnotationRange | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AtomCommand, InsertAtomParams, ResizeAtomParams, UpdateAtomParams } from "./types.js";
|
|
2
|
+
export declare const insertAtom: AtomCommand<InsertAtomParams>;
|
|
3
|
+
export declare const updateAtom: AtomCommand<UpdateAtomParams>;
|
|
4
|
+
export declare const deleteAtom: AtomCommand<Record<string, never>>;
|
|
5
|
+
export declare const resizeAtom: AtomCommand<ResizeAtomParams>;
|
|
6
|
+
export declare const atomCommands: {
|
|
7
|
+
readonly "atom.insert": AtomCommand<InsertAtomParams>;
|
|
8
|
+
readonly "atom.update": AtomCommand<UpdateAtomParams>;
|
|
9
|
+
readonly "atom.delete": AtomCommand<Record<string, never>>;
|
|
10
|
+
readonly "atom.resize": AtomCommand<ResizeAtomParams>;
|
|
11
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { isTextNode } from "../identity.js";
|
|
2
|
+
import { atomDeclarations } from "./declarations.js";
|
|
3
|
+
const locate = (document, id, ctx) => {
|
|
4
|
+
const resolved = ctx.positions.positionOf(id);
|
|
5
|
+
if (!resolved)
|
|
6
|
+
return null;
|
|
7
|
+
if (resolved.kind === "inline") {
|
|
8
|
+
let inlineOffset = 0;
|
|
9
|
+
for (let index = 0; index < (resolved.parent.children?.length || 0); index += 1) {
|
|
10
|
+
const child = resolved.parent.children[index];
|
|
11
|
+
if (!isTextNode(child) && child.id === id && inlineOffset === resolved.pos.offset) {
|
|
12
|
+
return { node: child, pos: { path: [...resolved.pos.path], offset: index } };
|
|
13
|
+
}
|
|
14
|
+
inlineOffset += isTextNode(child) ? child.text.length : 1;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const node = resolved.parent.children?.[resolved.pos.offset];
|
|
19
|
+
if (!node || isTextNode(node) || node.id !== id)
|
|
20
|
+
return null;
|
|
21
|
+
return { node, pos: resolved.pos };
|
|
22
|
+
};
|
|
23
|
+
const cleanAttrs = (attrs) => Object.fromEntries(Object.entries(attrs).filter(([, value]) => value !== undefined));
|
|
24
|
+
const inlineChildrenWithAtom = (owner, offset, atom) => {
|
|
25
|
+
if (!Number.isInteger(offset) || offset < 0)
|
|
26
|
+
return null;
|
|
27
|
+
const output = [];
|
|
28
|
+
let cursor = 0;
|
|
29
|
+
let inserted = false;
|
|
30
|
+
for (const child of owner.children || []) {
|
|
31
|
+
const width = isTextNode(child) ? child.text.length : 1;
|
|
32
|
+
if (!inserted && offset >= cursor && offset <= cursor + width) {
|
|
33
|
+
if (isTextNode(child)) {
|
|
34
|
+
const local = offset - cursor;
|
|
35
|
+
if (local)
|
|
36
|
+
output.push({ ...child, text: child.text.slice(0, local) });
|
|
37
|
+
output.push(atom);
|
|
38
|
+
if (local < child.text.length)
|
|
39
|
+
output.push({ ...child, text: child.text.slice(local) });
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
if (offset === cursor)
|
|
43
|
+
output.push(atom, child);
|
|
44
|
+
else if (offset === cursor + 1)
|
|
45
|
+
output.push(child, atom);
|
|
46
|
+
else
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
inserted = true;
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
output.push(child);
|
|
53
|
+
cursor += width;
|
|
54
|
+
}
|
|
55
|
+
if (!inserted && offset === cursor)
|
|
56
|
+
output.push(atom);
|
|
57
|
+
return inserted || offset === cursor ? output : null;
|
|
58
|
+
};
|
|
59
|
+
export const insertAtom = (document, _scope, params, ctx) => {
|
|
60
|
+
const attrs = cleanAttrs(params.attrs);
|
|
61
|
+
if (!params.declaration.validate(attrs))
|
|
62
|
+
return [];
|
|
63
|
+
const node = { type: params.declaration.type, id: params.nodeId, attrs };
|
|
64
|
+
if (params.declaration.group === "inline") {
|
|
65
|
+
if (!params.ownerId || params.offset === undefined)
|
|
66
|
+
return [];
|
|
67
|
+
const owner = locate(document, params.ownerId, ctx);
|
|
68
|
+
if (!owner)
|
|
69
|
+
return [];
|
|
70
|
+
const children = inlineChildrenWithAtom(owner.node, params.offset, node);
|
|
71
|
+
return children ? [{ type: "replaceNode", pos: owner.pos, before: owner.node, after: { ...owner.node, children } }] : [];
|
|
72
|
+
}
|
|
73
|
+
if (!params.parentId || params.index === undefined)
|
|
74
|
+
return [];
|
|
75
|
+
const range = ctx.positions.contentRangeOf(params.parentId);
|
|
76
|
+
const parentPosition = ctx.positions.positionOf(params.parentId);
|
|
77
|
+
const parent = parentPosition && parentPosition.parent.id === params.parentId
|
|
78
|
+
? parentPosition.parent
|
|
79
|
+
: parentPosition?.parent.children?.[parentPosition.pos.offset];
|
|
80
|
+
if (!range || !parent || isTextNode(parent) || params.index < 0 || params.index > (parent.children?.length || 0))
|
|
81
|
+
return [];
|
|
82
|
+
return [{ type: "insertNode", pos: { path: [...range.from.path], offset: params.index }, node }];
|
|
83
|
+
};
|
|
84
|
+
const atomIds = (scope) => scope.kind === "atomic-node" ? [scope.nodeId]
|
|
85
|
+
: scope.kind === "mixed" ? scope.parts.flatMap(atomIds) : [];
|
|
86
|
+
export const updateAtom = (document, scope, params, ctx) => atomIds(scope).flatMap((id) => {
|
|
87
|
+
const located = locate(document, id, ctx);
|
|
88
|
+
if (!located || ctx.schema.nodes[located.node.type]?.atomic !== true)
|
|
89
|
+
return [];
|
|
90
|
+
const after = cleanAttrs({ ...(located.node.attrs || {}), ...params.attrs });
|
|
91
|
+
const declaration = atomDeclarations.find((entry) => entry.type === located.node.type);
|
|
92
|
+
if (declaration && !declaration.validate(after))
|
|
93
|
+
return [];
|
|
94
|
+
return [{ type: "setNodeAttributes", pos: { path: [...located.pos.path, located.pos.offset], offset: 0 }, before: located.node.attrs || {}, after }];
|
|
95
|
+
});
|
|
96
|
+
export const deleteAtom = (document, scope, _params, ctx) => atomIds(scope).flatMap((id) => {
|
|
97
|
+
const located = locate(document, id, ctx);
|
|
98
|
+
return located && ctx.schema.nodes[located.node.type]?.atomic === true
|
|
99
|
+
? [{ type: "removeNode", pos: located.pos, node: located.node }] : [];
|
|
100
|
+
});
|
|
101
|
+
export const resizeAtom = (document, scope, params, ctx) => {
|
|
102
|
+
const id = atomIds(scope)[0];
|
|
103
|
+
const located = id ? locate(document, id, ctx) : null;
|
|
104
|
+
if (!located)
|
|
105
|
+
return [];
|
|
106
|
+
const beforeWidth = Number(located.node.attrs?.width) || params.width;
|
|
107
|
+
const beforeHeight = Number(located.node.attrs?.height) || params.height;
|
|
108
|
+
const minWidth = Math.max(1, params.minWidth || 16);
|
|
109
|
+
const minHeight = Math.max(1, params.minHeight || 16);
|
|
110
|
+
let width = Math.max(minWidth, params.width);
|
|
111
|
+
let height = Math.max(minHeight, params.height);
|
|
112
|
+
if (params.preserveAspectRatio && beforeWidth > 0 && beforeHeight > 0)
|
|
113
|
+
height = width * beforeHeight / beforeWidth;
|
|
114
|
+
if (![width, height].every((value) => Number.isFinite(value) && value <= 100000))
|
|
115
|
+
return [];
|
|
116
|
+
return updateAtom(document, scope, { attrs: { width, height } }, ctx);
|
|
117
|
+
};
|
|
118
|
+
export const atomCommands = { "atom.insert": insertAtom, "atom.update": updateAtom, "atom.delete": deleteAtom, "atom.resize": resizeAtom };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SmartElementNode } from "../types.js";
|
|
2
|
+
import type { CompositionToken } from "./types.js";
|
|
3
|
+
/** Mapping-aware tokens: atoms occupy one opaque unit and are never flattened. */
|
|
4
|
+
export declare const tokenizeCompositionOwner: (owner: SmartElementNode) => CompositionToken[];
|
|
5
|
+
export declare const compositionSegmentAt: (tokens: readonly CompositionToken[], offset: number) => {
|
|
6
|
+
from: number;
|
|
7
|
+
to: number;
|
|
8
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isTextNode } from "../identity.js";
|
|
2
|
+
/** Mapping-aware tokens: atoms occupy one opaque unit and are never flattened. */
|
|
3
|
+
export const tokenizeCompositionOwner = (owner) => (owner.children || []).map((child) => isTextNode(child)
|
|
4
|
+
? { kind: "text", text: child.text, marks: [...(child.marks || [])] }
|
|
5
|
+
: { kind: "atom", nodeId: child.id, atomType: child.type });
|
|
6
|
+
export const compositionSegmentAt = (tokens, offset) => {
|
|
7
|
+
let cursor = 0;
|
|
8
|
+
for (const token of tokens) {
|
|
9
|
+
const width = token.kind === "atom" ? 1 : token.text.length;
|
|
10
|
+
if (token.kind === "atom" && (offset === cursor || offset === cursor + 1))
|
|
11
|
+
return { from: offset, to: offset };
|
|
12
|
+
cursor += width;
|
|
13
|
+
}
|
|
14
|
+
cursor = 0;
|
|
15
|
+
for (const token of tokens) {
|
|
16
|
+
const width = token.kind === "atom" ? 1 : token.text.length;
|
|
17
|
+
if (token.kind === "text" && offset >= cursor && offset <= cursor + width)
|
|
18
|
+
return { from: cursor, to: cursor + width };
|
|
19
|
+
cursor += width;
|
|
20
|
+
}
|
|
21
|
+
return { from: cursor, to: cursor };
|
|
22
|
+
};
|