From 5496497d4b5f6c7fcdf09aa60a7fa4043b314b99 Mon Sep 17 00:00:00 2001 From: gohabereg Date: Sat, 20 Jun 2026 16:43:23 +0100 Subject: [PATCH 01/13] Extract common types from model to new package --- .github/workflows/model-types.yml | 14 + .gitignore | 2 + .husky/pre-commit | 1 + README.md | 5 +- docs/architecture.md | 11 +- docs/diagrams/architecture-overview.mmd | 31 +- docs/diagrams/events-catalog.mmd | 13 +- docs/plugins.md | 4 + package.json | 6 +- .../collaboration-manager/eslint.config.mjs | 2 +- packages/collaboration-manager/package.json | 6 +- .../src/BatchedOperation.spec.ts | 4 +- .../src/CollaborationManager.spec.ts | 2 +- .../src/CollaborationManager.ts | 2 +- .../src/Operation.spec.ts | 4 +- .../collaboration-manager/src/Operation.ts | 4 +- .../src/OperationsTransformer.spec.ts | 4 +- .../src/OperationsTransformer.ts | 2 +- .../src/UndoRedoManager.spec.ts | 4 +- .../src/client/Message.ts | 2 +- .../src/client/OTClient.spec.ts | 4 +- .../src/client/OTClient.ts | 2 +- .../src/utils/getRangesIntersectionType.ts | 2 +- .../test/mocks/createManager.ts | 19 +- .../collaboration-manager/tsconfig.build.json | 3 - packages/collaboration-manager/tsconfig.json | 5 + packages/core/package.json | 6 +- .../src/api/BlocksAPI.integration.spec.ts | 16 +- packages/core/src/api/BlocksAPI.ts | 15 +- .../src/api/DocumentAPI/DocumentAPI.spec.ts | 10 +- .../core/src/api/DocumentAPI/DocumentAPI.ts | 8 +- packages/core/src/api/SelectionAPI.spec.ts | 15 +- packages/core/src/api/SelectionAPI.ts | 5 +- packages/core/src/api/TextAPI.ts | 9 +- .../core/src/components/BlockManager.spec.ts | 64 +- packages/core/src/components/BlockManager.ts | 13 +- .../core/src/components/BlockRenderer.spec.ts | 42 +- packages/core/src/components/BlockRenderer.ts | 4 +- .../src/components/SelectionManager.spec.ts | 43 +- .../core/src/components/SelectionManager.ts | 21 +- .../src/components/UndoRedoManager.spec.ts | 24 +- .../core/src/components/UndoRedoManager.ts | 12 +- .../internal/block-tools/paragraph/index.ts | 4 +- .../tools/internal/inline-tools/bold/index.ts | 7 +- .../internal/inline-tools/italic/index.ts | 7 +- .../tools/internal/inline-tools/link/index.ts | 12 +- .../core/src/utils/composeDataFromVersion2.ts | 5 +- packages/dom-adapters/package.json | 5 +- .../src/BlockToolAdapter/EventTarget.d.ts | 2 +- .../src/BlockToolAdapter/index.ts | 2 +- .../dom-adapters/src/CaretAdapter/index.ts | 2 +- .../src/FormattingAdapter/index.ts | 6 +- .../dom-adapters/src/InputsRegistry/index.ts | 2 +- packages/dom-adapters/src/index.ts | 2 +- .../src/utils/doRangesIntersect.ts | 2 +- .../dom-adapters/src/utils/mergeTextRanges.ts | 2 +- .../src/utils/selectionRangeInInput.ts | 2 +- packages/dom-adapters/src/utils/surround.ts | 2 +- packages/model-types/.gitignore | 24 + packages/model-types/eslint.config.mjs | 35 + packages/model-types/jest.config.ts | 22 + packages/model-types/package.json | 30 + .../src/BaseDocumentEvent.ts} | 22 +- packages/model-types/src/BlockChildType.ts | 10 + packages/model-types/src/BlockId.ts | 35 + packages/model-types/src/BlockNode.ts | 38 + packages/model-types/src/BlockTune.ts | 13 + packages/model-types/src/Caret.ts | 15 + packages/model-types/src/ChangeData.ts | 7 + packages/model-types/src/DataKey.ts | 7 + packages/model-types/src/EditorDocument.ts | 24 + .../types => model-types/src}/EventAction.ts | 0 packages/model-types/src/EventBus.ts | 19 + packages/model-types/src/EventMap.ts | 47 + .../types => model-types/src}/EventType.ts | 0 packages/model-types/src/FormattingAction.ts | 7 + .../src}/Index/Index.spec.ts | 6 +- .../src}/Index/IndexBuilder.spec.ts | 4 +- .../model-types/src/Index/IndexBuilder.ts | 117 + .../src}/Index/index.ts | 32 +- packages/model-types/src/InlineTool.ts | 11 + packages/model-types/src/IntersectType.ts | 9 + .../src/utils => model-types/src}/Nominal.ts | 23 +- packages/model-types/src/Text.ts | 29 + packages/model-types/src/Value.ts | 10 + .../src}/events/BlockAddedEvent.ts | 9 +- .../src}/events/BlockRemovedEvent.ts | 11 +- .../events/CaretManagerCaretAddedEvent.ts | 7 +- .../events/CaretManagerCaretRemovedEvent.ts | 7 +- .../events/CaretManagerCaretUpdatedEvent.ts | 31 + .../src}/events/DataNodeAddedEvent.ts | 9 +- .../src}/events/DataNodeRemovedEvent.ts | 9 +- .../src}/events/PropertyModifiedEvent.ts | 8 +- .../src}/events/TextAddedEvent.ts | 10 +- .../src/events/TextFormattedEvent.ts | 34 + .../src}/events/TextRemovedEvent.ts | 10 +- .../src/events/TextUnformattedEvent.ts | 34 + .../src}/events/TuneModifiedEvent.ts | 8 +- .../src}/events/ValueModifiedEvent.ts | 8 +- packages/model-types/src/events/index.ts | 14 + packages/model-types/src/index.ts | 106 + packages/model-types/src/indexing.ts | 11 + .../utils => model-types/src}/keypath.spec.ts | 14 +- .../src/utils => model-types/src}/keypath.ts | 30 +- packages/model-types/tsconfig.build.json | 13 + packages/model-types/tsconfig.eslint.json | 15 + packages/model-types/tsconfig.json | 18 + packages/model/jest.config.ts | 13 +- packages/model/package.json | 9 +- .../src/CaretManagement/Caret/Caret.spec.ts | 2 +- .../model/src/CaretManagement/Caret/Caret.ts | 23 +- .../model/src/CaretManagement/Caret/types.ts | 16 +- .../src/CaretManagement/CaretManager.spec.ts | 8 +- .../model/src/CaretManagement/CaretManager.ts | 6 +- packages/model/src/CaretManagement/index.ts | 1 - .../src/EditorJSModel.integration.spec.ts | 4 +- packages/model/src/EditorJSModel.spec.ts | 5 +- packages/model/src/EditorJSModel.ts | 26 +- packages/model/src/EventBus/EventBus.ts | 5 - .../events/CaretManagerCaretUpdatedEvent.ts | 18 - .../src/EventBus/events/TextFormattedEvent.ts | 24 - .../EventBus/events/TextUnformattedEvent.ts | 24 - packages/model/src/EventBus/events/index.ts | 15 - packages/model/src/EventBus/index.ts | 5 +- packages/model/src/EventBus/types/EventMap.ts | 36 +- packages/model/src/EventBus/types/indexing.ts | 17 +- .../BlockNode/BlockNode.integration.spec.ts | 7 +- .../src/entities/BlockNode/BlockNode.spec.ts | 33 +- .../src/entities/BlockNode/__mocks__/index.ts | 8 +- .../model/src/entities/BlockNode/consts.ts | 4 - .../errors/AlreadyExistingKeyError.ts | 2 +- .../BlockNode/errors/InvalidNodeTypeError.ts | 2 +- .../BlockNode/errors/NonExistingKeyError.ts | 2 +- .../model/src/entities/BlockNode/index.ts | 50 +- .../BlockNode/types/BlockChildType.ts | 4 - .../src/entities/BlockNode/types/BlockId.ts | 45 - .../types/BlockNodeConstructorParameters.ts | 2 +- .../entities/BlockNode/types/BlockNodeData.ts | 2 +- .../BlockNode/types/BlockNodeSerialized.ts | 61 +- .../entities/BlockNode/types/BlockToolName.ts | 17 - .../src/entities/BlockNode/types/DataKey.ts | 20 - .../src/entities/BlockNode/types/index.ts | 13 +- .../src/entities/BlockTune/BlockTune.spec.ts | 6 +- .../src/entities/BlockTune/__mocks__/index.ts | 2 +- .../model/src/entities/BlockTune/index.ts | 6 +- .../types/BlockTuneConstructorParameters.ts | 3 +- .../entities/BlockTune/types/BlockTuneName.ts | 17 - .../BlockTune/types/BlockTuneSerialized.ts | 4 - .../src/entities/BlockTune/types/index.ts | 6 +- .../EditorDocument/EditorDocument.spec.ts | 46 +- .../errors/BlockAlreadyExistsError.ts | 2 +- .../src/entities/EditorDocument/index.ts | 48 +- .../EditorDocumentConstructorParameters.ts | 2 +- .../types/EditorDocumentSerialized.ts | 24 - .../EditorDocument/types/Properties.ts | 4 - .../entities/EditorDocument/types/index.ts | 3 +- .../model/src/entities/Index/IndexBuilder.ts | 116 - .../src/entities/ValueNode/ValueNode.spec.ts | 14 +- .../src/entities/ValueNode/__mocks__/index.ts | 2 +- .../model/src/entities/ValueNode/index.ts | 16 +- .../ValueNode/types/ValueSerialized.ts | 9 - .../src/entities/ValueNode/types/index.ts | 1 - packages/model/src/entities/index.ts | 2 - .../FormattingInlineNode/__mocks__/index.ts | 4 +- .../FormattingInlineNode/index.ts | 9 +- .../types/FormattingAction.ts | 15 - ...rmattingInlineNodeConstructorParameters.ts | 4 +- .../types/InlineToolData.ts | 17 - .../types/InlineToolName.ts | 17 - .../types/IntersectType.ts | 19 - .../FormattingInlineNode/types/index.ts | 6 - .../inline-fragments/InlineNode/index.ts | 52 +- .../ParentInlineNode/__mocks__/index.ts | 3 +- .../ParentInlineNode/index.ts | 12 +- .../inline-fragments/TextInlineNode/index.ts | 4 +- .../TextNode/TextNode.spec.ts | 7 +- .../TextNode/__mocks__/index.ts | 2 +- .../inline-fragments/TextNode/index.ts | 6 +- .../src/entities/inline-fragments/index.ts | 2 +- .../inline-fragments/specs/ChildNode.spec.ts | 8 +- .../specs/FormattingInlineNode.spec.ts | 33 +- .../specs/InlineTree.integration.spec.ts | 14 +- .../specs/ParentInlineNode.spec.ts | 122 +- .../inline-fragments/specs/ParentNode.spec.ts | 10 +- .../specs/TextInlineNode.spec.ts | 24 +- .../inline-fragments/specs/TextNode.spec.ts | 2 +- packages/model/src/mocks/data.ts | 2 +- .../model/src/tools/ToolsRegistry.spec.ts | 2 +- packages/model/src/tools/ToolsRegistry.ts | 2 +- packages/model/src/utils/index.ts | 4 +- packages/model/src/utils/textUtils.spec.ts | 6 +- packages/model/src/utils/textUtils.ts | 2 +- packages/model/tsconfig.json | 5 + packages/ot-server/package.json | 5 +- .../ot-server/src/DocumentManager.spec.ts | 2 +- packages/playground/package.json | 1 + packages/playground/src/App.vue | 8 +- .../playground/src/components/CaretIndex.vue | 3 +- packages/sdk/package.json | 6 +- packages/sdk/src/api/BlocksAPI.ts | 6 +- packages/sdk/src/api/DocumentAPI.ts | 7 +- packages/sdk/src/api/SelectionAPI.ts | 2 +- packages/sdk/src/api/TextAPI.ts | 2 +- packages/sdk/src/entities/BlockTool.ts | 2 +- packages/sdk/src/entities/BlockToolAdapter.ts | 6 +- packages/sdk/src/entities/Config.ts | 3 +- .../sdk/src/entities/EditorjsAdapterPlugin.ts | 2 +- .../sdk/src/entities/EventBus/EventBus.ts | 7 +- .../events/adapter/ValueNodeChanged.ts | 2 +- .../events/core/SelectionChangedCoreEvent.ts | 3 +- packages/sdk/src/entities/InlineTool.ts | 3 +- packages/sdk/src/entities/index.ts | 2 + packages/sdk/src/index.ts | 61 + .../src/tools/facades/BaseToolFacade.spec.ts | 4 +- .../sdk/src/tools/facades/BlockToolFacade.ts | 5 +- packages/sdk/src/utils/index.ts | 2 + packages/sdk/src/utils/keypath.ts | 3 + packages/sdk/tsconfig.build.json | 2 +- packages/sdk/tsconfig.json | 2 +- packages/ui/package.json | 1 - .../ui/src/InlineToolbar/InlineToolbar.ts | 4 +- packages/ui/tsconfig.build.json | 2 +- packages/ui/tsconfig.json | 3 - yarn.config.cjs | 45 + yarn.lock | 2618 +++++++++++++---- 225 files changed, 3812 insertions(+), 1830 deletions(-) create mode 100644 .github/workflows/model-types.yml create mode 100755 .husky/pre-commit create mode 100644 packages/model-types/.gitignore create mode 100644 packages/model-types/eslint.config.mjs create mode 100644 packages/model-types/jest.config.ts create mode 100644 packages/model-types/package.json rename packages/{model/src/EventBus/events/BaseEvent.ts => model-types/src/BaseDocumentEvent.ts} (63%) create mode 100644 packages/model-types/src/BlockChildType.ts create mode 100644 packages/model-types/src/BlockId.ts create mode 100644 packages/model-types/src/BlockNode.ts create mode 100644 packages/model-types/src/BlockTune.ts create mode 100644 packages/model-types/src/Caret.ts create mode 100644 packages/model-types/src/ChangeData.ts create mode 100644 packages/model-types/src/DataKey.ts create mode 100644 packages/model-types/src/EditorDocument.ts rename packages/{model/src/EventBus/types => model-types/src}/EventAction.ts (100%) create mode 100644 packages/model-types/src/EventBus.ts create mode 100644 packages/model-types/src/EventMap.ts rename packages/{model/src/EventBus/types => model-types/src}/EventType.ts (100%) create mode 100644 packages/model-types/src/FormattingAction.ts rename packages/{model/src/entities => model-types/src}/Index/Index.spec.ts (98%) rename packages/{model/src/entities => model-types/src}/Index/IndexBuilder.spec.ts (93%) create mode 100644 packages/model-types/src/Index/IndexBuilder.ts rename packages/{model/src/entities => model-types/src}/Index/index.ts (86%) create mode 100644 packages/model-types/src/InlineTool.ts create mode 100644 packages/model-types/src/IntersectType.ts rename packages/{model/src/utils => model-types/src}/Nominal.ts (50%) create mode 100644 packages/model-types/src/Text.ts create mode 100644 packages/model-types/src/Value.ts rename packages/{model/src/EventBus => model-types/src}/events/BlockAddedEvent.ts (61%) rename packages/{model/src/EventBus => model-types/src}/events/BlockRemovedEvent.ts (55%) rename packages/{model/src/EventBus => model-types/src}/events/CaretManagerCaretAddedEvent.ts (60%) rename packages/{model/src/EventBus => model-types/src}/events/CaretManagerCaretRemovedEvent.ts (60%) create mode 100644 packages/model-types/src/events/CaretManagerCaretUpdatedEvent.ts rename packages/{model/src/EventBus => model-types/src}/events/DataNodeAddedEvent.ts (61%) rename packages/{model/src/EventBus => model-types/src}/events/DataNodeRemovedEvent.ts (61%) rename packages/{model/src/EventBus => model-types/src}/events/PropertyModifiedEvent.ts (70%) rename packages/{model/src/EventBus => model-types/src}/events/TextAddedEvent.ts (56%) create mode 100644 packages/model-types/src/events/TextFormattedEvent.ts rename packages/{model/src/EventBus => model-types/src}/events/TextRemovedEvent.ts (57%) create mode 100644 packages/model-types/src/events/TextUnformattedEvent.ts rename packages/{model/src/EventBus => model-types/src}/events/TuneModifiedEvent.ts (69%) rename packages/{model/src/EventBus => model-types/src}/events/ValueModifiedEvent.ts (69%) create mode 100644 packages/model-types/src/events/index.ts create mode 100644 packages/model-types/src/index.ts create mode 100644 packages/model-types/src/indexing.ts rename packages/{model/src/utils => model-types/src}/keypath.spec.ts (95%) rename packages/{model/src/utils => model-types/src}/keypath.ts (80%) create mode 100644 packages/model-types/tsconfig.build.json create mode 100644 packages/model-types/tsconfig.eslint.json create mode 100644 packages/model-types/tsconfig.json delete mode 100644 packages/model/src/EventBus/EventBus.ts delete mode 100644 packages/model/src/EventBus/events/CaretManagerCaretUpdatedEvent.ts delete mode 100644 packages/model/src/EventBus/events/TextFormattedEvent.ts delete mode 100644 packages/model/src/EventBus/events/TextUnformattedEvent.ts delete mode 100644 packages/model/src/EventBus/events/index.ts delete mode 100644 packages/model/src/entities/BlockNode/consts.ts delete mode 100644 packages/model/src/entities/BlockNode/types/BlockChildType.ts delete mode 100644 packages/model/src/entities/BlockNode/types/BlockId.ts delete mode 100644 packages/model/src/entities/BlockNode/types/BlockToolName.ts delete mode 100644 packages/model/src/entities/BlockNode/types/DataKey.ts delete mode 100644 packages/model/src/entities/BlockTune/types/BlockTuneName.ts delete mode 100644 packages/model/src/entities/BlockTune/types/BlockTuneSerialized.ts delete mode 100644 packages/model/src/entities/EditorDocument/types/EditorDocumentSerialized.ts delete mode 100644 packages/model/src/entities/EditorDocument/types/Properties.ts delete mode 100644 packages/model/src/entities/Index/IndexBuilder.ts delete mode 100644 packages/model/src/entities/ValueNode/types/ValueSerialized.ts delete mode 100644 packages/model/src/entities/inline-fragments/FormattingInlineNode/types/FormattingAction.ts delete mode 100644 packages/model/src/entities/inline-fragments/FormattingInlineNode/types/InlineToolData.ts delete mode 100644 packages/model/src/entities/inline-fragments/FormattingInlineNode/types/InlineToolName.ts delete mode 100644 packages/model/src/entities/inline-fragments/FormattingInlineNode/types/IntersectType.ts create mode 100644 packages/sdk/src/utils/index.ts create mode 100644 packages/sdk/src/utils/keypath.ts create mode 100644 yarn.config.cjs diff --git a/.github/workflows/model-types.yml b/.github/workflows/model-types.yml new file mode 100644 index 00000000..f496c7fa --- /dev/null +++ b/.github/workflows/model-types.yml @@ -0,0 +1,14 @@ +name: Model-types check +on: + pull_request: + merge_group: + +jobs: + package-check: + uses: ./.github/workflows/package-check.yml + with: + package-name: '@editorjs/model-types' + working-directory: './packages/model-types' + include-mutations: false + secrets: + stryker_dashboard_api_key: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} diff --git a/.gitignore b/.gitignore index ccbe8ec4..063e5b71 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ jest-report.json # ENV **/.env +/.claude/ +/--help/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..e71709d0 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +yarn constraints diff --git a/README.md b/README.md index 2cc77a58..638161fc 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ A model-driven, collaboration-ready Editor.js engine split into focused packages | Package | Description | |---|---| -| [`@editorjs/sdk`](packages/sdk) | Shared contracts — interfaces, base event classes, `EventBus` | -| [`@editorjs/model`](packages/model) | In-memory document model (`EditorJSModel`, `BlockNode`, `TextNode`, caret management) | +| [`@editorjs/model-types`](packages/model-types) | Shared low-level types and base event classes used internally by `model` and `sdk` only — not intended for direct use by other packages or tools | +| [`@editorjs/sdk`](packages/sdk) | Shared contracts — interfaces, base event classes, `EventBus`. The package tools and plugins should depend on | +| [`@editorjs/model`](packages/model) | In-memory document model (`EditorJSModel`, `BlockNode`, `TextNode`, caret management). Internal engine used by `core`/`ot-server` — tools and plugins should use `@editorjs/sdk` instead | | [`@editorjs/dom-adapters`](packages/dom-adapters) | Binds model nodes to DOM inputs (`DOMBlockToolAdapter`, `CaretAdapter`, `FormattingAdapter`) | | [`@editorjs/collaboration-manager`](packages/collaboration-manager) | Operational transformation, batching, undo/redo, OT WebSocket client | | [`@editorjs/core`](packages/core) | Orchestrator — IoC container, plugin/tool lifecycle, `EditorAPI` | diff --git a/docs/architecture.md b/docs/architecture.md index d546b411..d355ba79 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,9 +1,10 @@ # Architecture Overview -The editor is split into eight packages in a layered dependency direction. +The editor is split into nine packages in a layered dependency direction. | Package | Role | |---|---| +| `@editorjs/model-types` | Shared low-level types, nominal brands, and base event classes (`Index`, `BaseDocumentEvent`, the model event classes, `EventBus`). No runtime dependencies of its own. | | `@editorjs/sdk` | Shared contracts — interfaces, base event classes, `EventBus` | | `@editorjs/model` | In-memory document model (`EditorJSModel`) | | `@editorjs/dom-adapters` | Binds model nodes to DOM inputs; default adapter implementation | @@ -15,12 +16,14 @@ The editor is split into eight packages in a layered dependency direction. ## Dependency rules +- `model-types` is the foundation layer: it has no dependency on `model` or `sdk`, and **only `model` and `sdk` may depend on it directly**. Every other package (`dom-adapters`, `collaboration-manager`, `ui`, and tools/plugins in general) that needs `Index`, event classes, or other model-types primitives should get them re-exported through `@editorjs/sdk` instead of depending on `@editorjs/model-types` directly. This exists so `model` and `sdk` can share the same `Index`/event/nominal-type definitions without `sdk` depending on the full `model` engine (and vice versa) — see `packages/model-types/src/index.ts` for the exact re-exported surface. - `sdk` is the contract layer all other packages depend on. +- `model` is the engine implementation that backs `EditorJSModel`. It is consumed directly by `core` (the orchestrator) and `ot-server` (server-side document state), but **tools and plugins should never import `@editorjs/model` directly** — they should depend on `@editorjs/sdk`'s contracts (`BlockTool`, `InlineTool`, `Index`, event types, etc.) instead. `sdk` re-exports everything from `model` that a tool/plugin author legitimately needs, so `model` itself isn't part of the stable, tool-facing API surface and is free to change its internals. - `core` wires runtime dependencies; it should be the only orchestrator. - `model` does not depend on DOM concerns. -- `dom-adapters` and `collaboration-manager` observe/apply model changes through public APIs and events. -- `ui` depends on `sdk` and `model`; it is registered as an `EditorjsPlugin` via `core.use()`. -- `ot-server` depends on `collaboration-manager` (for `Operation` / message types) and `model`; it runs server-side only. +- `dom-adapters` and `collaboration-manager` observe/apply model changes through public APIs and events, and depend only on `sdk` (not `model` or `model-types`). +- `ui` depends on `sdk`; it is registered as an `EditorjsPlugin` via `core.use()`. +- `ot-server` depends on `collaboration-manager` (for `Operation` / message types), `model`, and `sdk`; it runs server-side only. ## Runtime ownership diff --git a/docs/diagrams/architecture-overview.mmd b/docs/diagrams/architecture-overview.mmd index a654be6f..219b189c 100644 --- a/docs/diagrams/architecture-overview.mmd +++ b/docs/diagrams/architecture-overview.mmd @@ -7,9 +7,10 @@ classDiagram direction LR - %% Layer 0 — Contracts (@editorjs/sdk) - namespace sdk { - + %% Layer 0 — Foundation (@editorjs/model-types) + %% Internal-only: depended on by `model` and `sdk` directly; every other + %% package (and all tools/plugins) should get these via `@editorjs/sdk` instead. + namespace modelTypes { class EventBus { <> +addEventListener(type, callback) @@ -17,6 +18,18 @@ classDiagram +dispatchEvent(event) } + class Index { + +blockIndex: number + +dataKey: DataKey + +textRange: TextRange + +serialize(): string + +parse(serialized): Index + } + } + + %% Layer 1 — Contracts (@editorjs/sdk) + namespace sdk { + class BlockToolAdapter { <> +attachInput(keyRaw, input: HTMLElement) @@ -52,7 +65,7 @@ classDiagram } } - %% Layer 1 — Data model (@editorjs/model) + %% Layer 2 — Data model (@editorjs/model) namespace model { class EditorJSModel { +serialized: EditorDocumentSerialized @@ -69,14 +82,14 @@ classDiagram } } - %% Layer 2a — DOM binding (@editorjs/dom-adapters) + %% Layer 3a — DOM binding (@editorjs/dom-adapters) namespace domAdapters { class DOMAdapters { +createBlockToolAdapter(blockIndex, toolName): BlockToolAdapter } } - %% Layer 2b — Collaboration plugin (@editorjs/collaboration-manager) + %% Layer 3b — Collaboration plugin (@editorjs/collaboration-manager) namespace collaborationManager { class CollaborationManager { <> @@ -87,7 +100,7 @@ classDiagram } } - %% Layer 3 — Orchestrator (@editorjs/core) + %% Layer 4 — Orchestrator (@editorjs/core) namespace core { class Core { +constructor(config) @@ -105,6 +118,10 @@ classDiagram EditorJSModel --|> EventBus : extends Core *-- EventBus : creates & holds + %% Foundation usage — model-types is depended on by model and sdk only + EditorJSModel ..> Index : uses (caret/selection addressing) + BlockToolAdapter ..> Index : uses (caret/selection addressing) + %% Core — owns & wires Core *-- EditorJSModel Core *-- UndoRedoManager : creates (listens on EventBus) diff --git a/docs/diagrams/events-catalog.mmd b/docs/diagrams/events-catalog.mmd index 1e771878..d2313c4b 100644 --- a/docs/diagrams/events-catalog.mmd +++ b/docs/diagrams/events-catalog.mmd @@ -9,6 +9,8 @@ classDiagram direction LR %% ── Base classes ───────────────────────────────── + %% BaseDocumentEvent is defined in @editorjs/model-types, like the + %% concrete events below it. class BaseDocumentEvent { <> detail.index: Index @@ -44,8 +46,10 @@ classDiagram <> } - %% ── @editorjs/model ────────────────────────────── - namespace model { + %% ── @editorjs/model-types ──────────────────────── + %% Defined here, internal-only; re-exported by both `model` (for + %% EditorJSModel to dispatch) and `sdk` (for tools/plugins to consume). + namespace modelTypes { class BlockAddedEvent { detail.data: BlockNodeSerialized } @@ -79,6 +83,10 @@ classDiagram class TuneModifiedEvent { detail.data.value: T detail.data.previous: T + } + class PropertyModifiedEvent { + detail.data.value: T + detail.data.previous: T } class CaretManagerCaretUpdatedEvent { <> @@ -156,6 +164,7 @@ classDiagram DataNodeRemovedEvent --|> BaseDocumentEvent ValueModifiedEvent --|> BaseDocumentEvent TuneModifiedEvent --|> BaseDocumentEvent + PropertyModifiedEvent --|> BaseDocumentEvent BlockAddedCoreEvent --|> CoreEventBase BlockRemovedCoreEvent --|> CoreEventBase diff --git a/docs/plugins.md b/docs/plugins.md index 1fbfd823..6447b7b5 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -1,5 +1,9 @@ # Plugins & Tools +## Package boundary + +Tools and plugins should only depend on `@editorjs/sdk` — never on `@editorjs/model` or `@editorjs/model-types` directly. `sdk` re-exports every type a tool/plugin author needs (`Index`, event classes, `BlockTool`/`InlineTool`/`BlockTune` contracts, etc.); `model` is the engine implementation that `core` and `ot-server` orchestrate, and `model-types` is an internal foundation shared only by `model` and `sdk`. Neither is part of the stable, tool-facing API. + ## Registration `core.use(...)` registers UI components/plugins by static `type` (values from `ToolType` for tools, `PluginType.Adapter` for adapters, and `PluginType.Plugin` for general plugins). diff --git a/package.json b/package.json index 552128ee..c20fc2f2 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,10 @@ "build": "yarn workspaces foreach -At run build", "test": "yarn workspaces foreach -A run test", "lint": "yarn workspaces foreach -A run lint", - "lint:fix": "yarn workspaces foreach -A run lint --fix" + "lint:fix": "yarn workspaces foreach -A run lint --fix", + "prepare": "husky" + }, + "devDependencies": { + "husky": "^9.1.7" } } diff --git a/packages/collaboration-manager/eslint.config.mjs b/packages/collaboration-manager/eslint.config.mjs index 0ea434ea..919de62e 100644 --- a/packages/collaboration-manager/eslint.config.mjs +++ b/packages/collaboration-manager/eslint.config.mjs @@ -47,7 +47,7 @@ export default [ * For test files allow dev dependencies imports */ 'n/no-unpublished-import': ['error', { - allowModules: ['@jest/globals'], + allowModules: ['@jest/globals', '@editorjs/model'], }], /** * Used for ws mock in test files diff --git a/packages/collaboration-manager/package.json b/packages/collaboration-manager/package.json index f60cde50..0251d8eb 100644 --- a/packages/collaboration-manager/package.json +++ b/packages/collaboration-manager/package.json @@ -18,20 +18,20 @@ "clear": "rm -rf ./dist && rm -rf ./tsconfig.build.tsbuildinfo" }, "dependencies": { - "@editorjs/model": "workspace:^", "@editorjs/sdk": "workspace:^" }, "devDependencies": { + "@editorjs/model": "workspace:^", "@jest/globals": "^29.7.0", "@stryker-mutator/core": "^7.0.2", "@stryker-mutator/jest-runner": "^7.0.2", "@stryker-mutator/typescript-checker": "^7.0.2", "@types/eslint": "^8", - "@types/jest": "^29.5.12", + "@types/jest": "^30.0.0", "eslint": "^8.38.0", "eslint-config-codex": "^2.0.2", "eslint-plugin-import": "^2.29.0", - "jest": "^29.7.0", + "jest": "^30.4.2", "stryker-cli": "^1.0.2", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", diff --git a/packages/collaboration-manager/src/BatchedOperation.spec.ts b/packages/collaboration-manager/src/BatchedOperation.spec.ts index aec84690..b973e9b6 100644 --- a/packages/collaboration-manager/src/BatchedOperation.spec.ts +++ b/packages/collaboration-manager/src/BatchedOperation.spec.ts @@ -1,5 +1,5 @@ -import type { Index } from '@editorjs/model'; -import { createDataKey, IndexBuilder, type TextRange } from '@editorjs/model'; +import type { Index } from '@editorjs/sdk'; +import { createDataKey, IndexBuilder, type TextRange } from '@editorjs/sdk'; import { BatchedOperation } from './BatchedOperation.js'; import type { SerializedOperation } from './Operation.js'; import { Operation, OperationType } from './Operation.js'; diff --git a/packages/collaboration-manager/src/CollaborationManager.spec.ts b/packages/collaboration-manager/src/CollaborationManager.spec.ts index 61480e72..83fd4209 100644 --- a/packages/collaboration-manager/src/CollaborationManager.spec.ts +++ b/packages/collaboration-manager/src/CollaborationManager.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ -import { createDataKey, EventType, IndexBuilder } from '@editorjs/model'; +import { createDataKey, EventType, IndexBuilder } from '@editorjs/sdk'; import { EditorJSModel } from '@editorjs/model'; import { CoreEventType, type CoreConfig } from '@editorjs/sdk'; import { beforeAll, jest } from '@jest/globals'; diff --git a/packages/collaboration-manager/src/CollaborationManager.ts b/packages/collaboration-manager/src/CollaborationManager.ts index 7f9dfa10..b3775ac2 100644 --- a/packages/collaboration-manager/src/CollaborationManager.ts +++ b/packages/collaboration-manager/src/CollaborationManager.ts @@ -5,7 +5,7 @@ import { TextAddedEvent, TextFormattedEvent, TextRemovedEvent, TextUnformattedEvent -} from '@editorjs/model'; +} from '@editorjs/sdk'; import type { UndoCoreEvent, EditorAPI, diff --git a/packages/collaboration-manager/src/Operation.spec.ts b/packages/collaboration-manager/src/Operation.spec.ts index 5fd90dd6..aa500e95 100644 --- a/packages/collaboration-manager/src/Operation.spec.ts +++ b/packages/collaboration-manager/src/Operation.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ -import type { BlockNodeSerialized, DataKey, DocumentIndex } from '@editorjs/model'; -import { IndexBuilder } from '@editorjs/model'; +import type { BlockNodeSerialized, DataKey, DocumentIndex } from '@editorjs/sdk'; +import { IndexBuilder } from '@editorjs/sdk'; import { describe } from '@jest/globals'; import { type InsertOrDeleteOperationData, type ModifyOperationData, Operation, OperationType } from './Operation.js'; diff --git a/packages/collaboration-manager/src/Operation.ts b/packages/collaboration-manager/src/Operation.ts index 749d63f0..8f5eae0e 100644 --- a/packages/collaboration-manager/src/Operation.ts +++ b/packages/collaboration-manager/src/Operation.ts @@ -1,5 +1,5 @@ -import type { TextRange } from '@editorjs/model'; -import { IndexBuilder, type Index, type BlockNodeSerialized } from '@editorjs/model'; +import type { TextRange } from '@editorjs/sdk'; +import { IndexBuilder, type Index, type BlockNodeSerialized } from '@editorjs/sdk'; import { OperationsTransformer } from './OperationsTransformer.js'; /** diff --git a/packages/collaboration-manager/src/OperationsTransformer.spec.ts b/packages/collaboration-manager/src/OperationsTransformer.spec.ts index 6420b872..40d5860f 100644 --- a/packages/collaboration-manager/src/OperationsTransformer.spec.ts +++ b/packages/collaboration-manager/src/OperationsTransformer.spec.ts @@ -1,5 +1,5 @@ -import type { DocumentId, Index } from '@editorjs/model'; -import { createDataKey, IndexBuilder } from '@editorjs/model'; +import type { DocumentId, Index } from '@editorjs/sdk'; +import { createDataKey, IndexBuilder } from '@editorjs/sdk'; import { Operation, OperationType } from './Operation.js'; import { OperationsTransformer } from './OperationsTransformer.js'; diff --git a/packages/collaboration-manager/src/OperationsTransformer.ts b/packages/collaboration-manager/src/OperationsTransformer.ts index 5b0c3bf6..4c0e3c23 100644 --- a/packages/collaboration-manager/src/OperationsTransformer.ts +++ b/packages/collaboration-manager/src/OperationsTransformer.ts @@ -1,4 +1,4 @@ -import { IndexBuilder } from '@editorjs/model'; +import { IndexBuilder } from '@editorjs/sdk'; import { Operation, OperationType } from './Operation.js'; import { getRangesIntersectionType, RangeIntersectionType } from './utils/getRangesIntersectionType.js'; diff --git a/packages/collaboration-manager/src/UndoRedoManager.spec.ts b/packages/collaboration-manager/src/UndoRedoManager.spec.ts index aaa4f341..09393402 100644 --- a/packages/collaboration-manager/src/UndoRedoManager.spec.ts +++ b/packages/collaboration-manager/src/UndoRedoManager.spec.ts @@ -1,5 +1,5 @@ -import type { DocumentId } from '@editorjs/model'; -import { createDataKey, IndexBuilder } from '@editorjs/model'; +import type { DocumentId } from '@editorjs/sdk'; +import { createDataKey, IndexBuilder } from '@editorjs/sdk'; import { describe } from '@jest/globals'; import { jest } from '@jest/globals'; import { Operation, OperationType } from './Operation.js'; diff --git a/packages/collaboration-manager/src/client/Message.ts b/packages/collaboration-manager/src/client/Message.ts index 2ce78580..702580a0 100644 --- a/packages/collaboration-manager/src/client/Message.ts +++ b/packages/collaboration-manager/src/client/Message.ts @@ -1,4 +1,4 @@ -import type { DocumentId, EditorDocumentSerialized } from '@editorjs/model'; +import type { DocumentId, EditorDocumentSerialized } from '@editorjs/sdk'; import type { MessageType } from './MessageType.js'; import type { SerializedOperation } from '../Operation.js'; diff --git a/packages/collaboration-manager/src/client/OTClient.spec.ts b/packages/collaboration-manager/src/client/OTClient.spec.ts index 18c9a6c4..1155df13 100644 --- a/packages/collaboration-manager/src/client/OTClient.spec.ts +++ b/packages/collaboration-manager/src/client/OTClient.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ -import type { DocumentId, EditorDocumentSerialized } from '@editorjs/model'; -import { createDataKey, IndexBuilder } from '@editorjs/model'; +import type { DocumentId, EditorDocumentSerialized } from '@editorjs/sdk'; +import { createDataKey, IndexBuilder } from '@editorjs/sdk'; import { beforeEach, afterEach, jest, describe, it, expect } from '@jest/globals'; import { OTClient } from './OTClient.js'; import { Operation, OperationType } from '../Operation.js'; diff --git a/packages/collaboration-manager/src/client/OTClient.ts b/packages/collaboration-manager/src/client/OTClient.ts index 7f04408b..708a21be 100644 --- a/packages/collaboration-manager/src/client/OTClient.ts +++ b/packages/collaboration-manager/src/client/OTClient.ts @@ -1,4 +1,4 @@ -import type { EditorDocumentSerialized } from '@editorjs/model'; +import type { EditorDocumentSerialized } from '@editorjs/sdk'; import { Operation, OperationType, type SerializedOperation } from '../Operation.js'; import type { HandshakeMessage, HandshakePayload, Message, OperationMessage } from './Message.js'; import { MessageType } from './MessageType.js'; diff --git a/packages/collaboration-manager/src/utils/getRangesIntersectionType.ts b/packages/collaboration-manager/src/utils/getRangesIntersectionType.ts index cc652f16..359792df 100644 --- a/packages/collaboration-manager/src/utils/getRangesIntersectionType.ts +++ b/packages/collaboration-manager/src/utils/getRangesIntersectionType.ts @@ -1,4 +1,4 @@ -import type { TextRange } from '@editorjs/model'; +import type { TextRange } from '@editorjs/sdk'; /** * Represents the type of intersection between two text ranges. diff --git a/packages/collaboration-manager/test/mocks/createManager.ts b/packages/collaboration-manager/test/mocks/createManager.ts index 7487a221..cd9ab1f1 100644 --- a/packages/collaboration-manager/test/mocks/createManager.ts +++ b/packages/collaboration-manager/test/mocks/createManager.ts @@ -1,15 +1,22 @@ -import type { EditorDocumentSerialized, ModelEvents } from '@editorjs/model'; -import { EventType } from '@editorjs/model'; -import type { EditorJSModel } from '@editorjs/model'; -import { EventBus } from '@editorjs/sdk'; +import type { EditorDocumentSerialized, ModelEvents, Index } from '@editorjs/sdk'; +import { EventBus, EventType } from '@editorjs/sdk'; import type { CoreConfigValidated, DocumentAPI, EditorAPI, InsertRemoveDataParams, ModifyDataParams, BlocksAPI } from '@editorjs/sdk'; import { CollaborationManager } from '../../src/CollaborationManager.js'; +interface EditorModel { + serialized: EditorDocumentSerialized; + addEventListener(type: string, callback: (event: ModelEvents) => void): void; + removeEventListener(type: string, callback: (event: ModelEvents) => void): void; + insertData(userId: string | number | undefined, index: Index, data: unknown): void; + removeData(userId: string | number | undefined, index: Index, data: unknown): void; + modifyData(userId: string | number | undefined, index: Index, data: unknown): void; +} + /** * Creates a mock DocumentAPI backed by a real EditorJSModel instance * @param model - the EditorJS model to back the mock API with */ -function createMockDocumentAPI(model: EditorJSModel): DocumentAPI { +function createMockDocumentAPI(model: EditorModel): DocumentAPI { return { get data(): EditorDocumentSerialized { return model.serialized; @@ -37,7 +44,7 @@ function createMockDocumentAPI(model: EditorJSModel): DocumentAPI { * @param model - the EditorJS model instance * @returns an object containing the manager and the eventBus used */ -export function createManager(config: CoreConfigValidated, model: EditorJSModel): { +export function createManager(config: CoreConfigValidated, model: EditorModel): { manager: CollaborationManager; eventBus: EventBus; } { diff --git a/packages/collaboration-manager/tsconfig.build.json b/packages/collaboration-manager/tsconfig.build.json index c53706de..5d61edc8 100644 --- a/packages/collaboration-manager/tsconfig.build.json +++ b/packages/collaboration-manager/tsconfig.build.json @@ -11,9 +11,6 @@ "src/**/*.spec.ts" ], "references": [ - { - "path": "../model/tsconfig.build.json" - }, { "path": "../sdk/tsconfig.build.json" } diff --git a/packages/collaboration-manager/tsconfig.json b/packages/collaboration-manager/tsconfig.json index 887e1c46..24b47a47 100644 --- a/packages/collaboration-manager/tsconfig.json +++ b/packages/collaboration-manager/tsconfig.json @@ -17,5 +17,10 @@ "node_modules/**/*", "dist/**/*", "**/*.spec.ts" + ], + "references": [ + { + "path": "../sdk/tsconfig.build.json" + } ] } diff --git a/packages/core/package.json b/packages/core/package.json index c165f2bd..701bb803 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,13 +25,13 @@ "@stryker-mutator/jest-runner": "^7.0.2", "@stryker-mutator/typescript-checker": "^7.0.2", "@types/eslint": "^8", - "@types/jest": "^29.5.1", + "@types/jest": "^30.0.0", "@types/node": "^22.10.2", - "babel-jest": "^30.3.0", + "babel-jest": "^30.4.1", "eslint": "^9.24.0", "eslint-config-codex": "^2.0.3", "eslint-plugin-import": "^2.29.0", - "jest": "^29.7.0", + "jest": "^30.4.2", "stryker-cli": "^1.0.2", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", diff --git a/packages/core/src/api/BlocksAPI.integration.spec.ts b/packages/core/src/api/BlocksAPI.integration.spec.ts index 4a54ff47..310087d0 100644 --- a/packages/core/src/api/BlocksAPI.integration.spec.ts +++ b/packages/core/src/api/BlocksAPI.integration.spec.ts @@ -5,8 +5,9 @@ import type { CoreConfigValidated } from '@editorjs/sdk'; import type { BlocksManager } from '../components/BlockManager'; // @ts-expect-error - TS don't import types via import() so have to import them here as well import type ToolsManager from '../tools/ToolsManager'; -import type { TextNodeSerialized } from '@editorjs/model'; - +import type { TextNodeSerialized } from '@editorjs/sdk'; +import { EventBus, EventType, BlockAddedEvent, BlockRemovedEvent } from '@editorjs/sdk'; +import { EditorJSModel } from '@editorjs/model'; const USER_ID = 'integration-user'; const DOCUMENT_ID = 'integration-doc'; @@ -15,14 +16,6 @@ const DOCUMENT_ID = 'integration-doc'; */ console.error = jest.fn(); -jest.unstable_mockModule('@editorjs/sdk', () => ({ - BlockAddedCoreEvent: jest.fn(), - BlockRemovedCoreEvent: jest.fn(), - EventBus: jest.fn(() => ({ - dispatchEvent: jest.fn(), - })), -})); - /** * Mock DOM adapters — they require a real DOM environment */ @@ -51,9 +44,6 @@ jest.unstable_mockModule('../tools/ToolsManager', () => ({ })), })); -// Import real model (no mock) and mocked adapters -const { EditorJSModel, EventType, BlockAddedEvent, BlockRemovedEvent } = await import('@editorjs/model'); -const { EventBus } = await import('@editorjs/sdk'); const ToolsManager = (await import('../tools/ToolsManager')).default; const { BlocksManager } = await import('../components/BlockManager.js'); const { BlocksAPI } = await import('./BlocksAPI.js'); diff --git a/packages/core/src/api/BlocksAPI.ts b/packages/core/src/api/BlocksAPI.ts index b61d8893..eb5e175c 100644 --- a/packages/core/src/api/BlocksAPI.ts +++ b/packages/core/src/api/BlocksAPI.ts @@ -2,18 +2,9 @@ import 'reflect-metadata'; import { inject, injectable } from 'inversify'; import { TOKENS } from '../tokens.js'; import { BlocksManager } from '../components/BlockManager.js'; -import { CoreConfigValidated } from '@editorjs/sdk'; -import { BlocksAPI as BlocksApiInterface } from '@editorjs/sdk'; -import { - BlockId, - BlockIndexOrId, - createBlockId, - createDataKey, - EditorDocumentSerialized, - EditorJSModel, - TextNodeSerialized, - ValueSerialized -} from '@editorjs/model'; +import { CoreConfigValidated, BlocksAPI as BlocksApiInterface, EditorDocumentSerialized, BlockId, BlockIndexOrId, createBlockId, createDataKey } from '@editorjs/sdk'; +import { EditorJSModel } from '@editorjs/model'; +import type { TextNodeSerialized, ValueSerialized } from '@editorjs/sdk'; /** * Blocks API diff --git a/packages/core/src/api/DocumentAPI/DocumentAPI.spec.ts b/packages/core/src/api/DocumentAPI/DocumentAPI.spec.ts index 77945ef7..c589ae66 100644 --- a/packages/core/src/api/DocumentAPI/DocumentAPI.spec.ts +++ b/packages/core/src/api/DocumentAPI/DocumentAPI.spec.ts @@ -10,6 +10,9 @@ jest.unstable_mockModule('@editorjs/sdk', () => ({ public name = 'redo'; }, EventBus: jest.fn(), + EventType: { + Changed: 'update', + }, })); jest.unstable_mockModule('@editorjs/model', () => { @@ -19,9 +22,6 @@ jest.unstable_mockModule('@editorjs/model', () => { return { EditorJSModel, - EventType: { - Changed: 'update', - }, }; }); @@ -73,7 +73,7 @@ describe('DocumentAPI', () => { it('should dispatch an undo core event', () => { documentAPI.undo(); - expect(dispatchEvent).toBeCalledWith(expect.objectContaining({ name: 'undo' })); + expect(dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({ name: 'undo' })); }); }); @@ -81,7 +81,7 @@ describe('DocumentAPI', () => { it('should dispatch an redo core event', () => { documentAPI.redo(); - expect(dispatchEvent).toBeCalledWith(expect.objectContaining({ name: 'redo' })); + expect(dispatchEvent).toHaveBeenCalledWith(expect.objectContaining({ name: 'redo' })); }); }); }); diff --git a/packages/core/src/api/DocumentAPI/DocumentAPI.ts b/packages/core/src/api/DocumentAPI/DocumentAPI.ts index c7863ba8..e1e26b98 100644 --- a/packages/core/src/api/DocumentAPI/DocumentAPI.ts +++ b/packages/core/src/api/DocumentAPI/DocumentAPI.ts @@ -1,11 +1,15 @@ import 'reflect-metadata'; -import { type EditorDocumentSerialized, EditorJSModel, EventType, type ModelEvents } from '@editorjs/model'; +import { EditorJSModel } from '@editorjs/model'; import { CoreConfigValidated, DocumentAPI as DocumentApiInterface, EventBus, + type EditorDocumentSerialized, + EventType, type InsertRemoveDataParams, - type ModifyDataParams, RedoCoreEvent, UndoCoreEvent + type ModifyDataParams, + type ModelEvents, + RedoCoreEvent, UndoCoreEvent } from '@editorjs/sdk'; import { inject, injectable } from 'inversify'; import { TOKENS } from '../../tokens.js'; diff --git a/packages/core/src/api/SelectionAPI.spec.ts b/packages/core/src/api/SelectionAPI.spec.ts index e01f09ab..04f3fd4f 100644 --- a/packages/core/src/api/SelectionAPI.spec.ts +++ b/packages/core/src/api/SelectionAPI.spec.ts @@ -3,6 +3,13 @@ import { jest } from '@jest/globals'; import type { CoreConfigValidated } from '@editorjs/sdk'; // Mock dependencies before importing the module under test +jest.unstable_mockModule('@editorjs/sdk', () => ({ + createInlineToolName: jest.fn((name: string) => `inline:${name}`), + EventType: { + CaretManagerUpdated: 'update', + }, +})); + jest.unstable_mockModule('../components/SelectionManager', () => ({ SelectionManager: jest.fn(() => ({ applyInlineToolForCurrentSelection: jest.fn(), @@ -11,15 +18,13 @@ jest.unstable_mockModule('../components/SelectionManager', () => ({ jest.unstable_mockModule('@editorjs/model', () => ({ EditorJSModel: jest.fn(), - createInlineToolName: jest.fn((name: string) => `inline:${name}`), - EventType: { - CaretManagerUpdated: 'update', - }, + Caret: class {}, })); const { SelectionAPI } = await import('./SelectionAPI.js'); const { SelectionManager } = await import('../components/SelectionManager'); -const { EditorJSModel, createInlineToolName } = await import('@editorjs/model'); +const { EditorJSModel } = await import('@editorjs/model'); +const { createInlineToolName } = await import('@editorjs/sdk'); describe('SelectionAPI', () => { // @ts-expect-error - mock object diff --git a/packages/core/src/api/SelectionAPI.ts b/packages/core/src/api/SelectionAPI.ts index 7c968e40..43befaa6 100644 --- a/packages/core/src/api/SelectionAPI.ts +++ b/packages/core/src/api/SelectionAPI.ts @@ -2,9 +2,8 @@ import 'reflect-metadata'; import { inject, injectable } from 'inversify'; import { SelectionManager } from '../components/SelectionManager.js'; -import { Caret, CaretManagerEvents, createInlineToolName, EditorJSModel, EventType } from '@editorjs/model'; -import { CoreConfigValidated } from '@editorjs/sdk'; -import { SelectionAPI as SelectionApiInterface } from '@editorjs/sdk'; +import { Caret, EditorJSModel } from '@editorjs/model'; +import { CaretManagerEvents, CoreConfigValidated, createInlineToolName, EventType, SelectionAPI as SelectionApiInterface } from '@editorjs/sdk'; import { TOKENS } from '../tokens.js'; /** diff --git a/packages/core/src/api/TextAPI.ts b/packages/core/src/api/TextAPI.ts index 91b44e32..02b9ee96 100644 --- a/packages/core/src/api/TextAPI.ts +++ b/packages/core/src/api/TextAPI.ts @@ -1,14 +1,15 @@ +import { EditorJSModel } from '@editorjs/model'; +import type { InlineFragment } from '@editorjs/sdk'; import { BlockIndexOrId, + CoreConfigValidated, createDataKey, createInlineToolData, createInlineToolName, - EditorJSModel, InlineFragment -} from '@editorjs/model'; -import type { CoreConfigValidated } from '@editorjs/sdk'; + TextAPI as TextAPIInterface +} from '@editorjs/sdk'; import { inject, injectable } from 'inversify'; import { TOKENS } from '../tokens.js'; -import { TextAPI as TextAPIInterface } from '@editorjs/sdk'; /** * Text API to work with the text content of the document diff --git a/packages/core/src/components/BlockManager.spec.ts b/packages/core/src/components/BlockManager.spec.ts index 8102c397..532d59af 100644 --- a/packages/core/src/components/BlockManager.spec.ts +++ b/packages/core/src/components/BlockManager.spec.ts @@ -1,13 +1,19 @@ /* eslint-disable @stylistic/comma-dangle,@typescript-eslint/naming-convention,@typescript-eslint/no-magic-numbers */ import { beforeEach, jest } from '@jest/globals'; import type { CoreConfigValidated } from '@editorjs/sdk'; -import type { DataKey } from '@editorjs/model'; +import type { DataKey } from '@editorjs/sdk'; const BLOCKS_COUNT = 7; const USER_ID = 'user'; jest.unstable_mockModule('@editorjs/sdk', () => ({ - EventBus: jest.fn(), + EventBus: jest.fn(() => ({ dispatchEvent: jest.fn() })), + EventType: { Changed: 'changed' }, + BlockChildType: { Text: 't' }, + NODE_TYPE_HIDDEN_PROP: '$t', + createBlockId: jest.fn((id: string) => id as never), + set: jest.fn(() => undefined), + renumberKeys: jest.fn(() => new Map()), })); // Register ESM mocks before importing the module under test @@ -30,31 +36,10 @@ jest.unstable_mockModule('@editorjs/model', () => { }, })); - const EventBus = jest.fn(() => ({ dispatchEvent: jest.fn() })); - - const EventType = { Changed: 'changed' }; - - const keypath = { - set: jest.fn(), - get: jest.fn(), - has: jest.fn(), - renumberKeys: jest.fn(() => new Map()), - }; - - const sliceFragments = jest.fn((frags: unknown[]) => frags); - const mergeTextNodes = jest.fn((_entries: unknown[], initial: unknown) => initial); - const NODE_TYPE_HIDDEN_PROP = '$t'; - const BlockChildType = { Text: 't' }; - return { EditorJSModel, - EventBus, - EventType, - keypath, - sliceFragments, - mergeTextNodes, - NODE_TYPE_HIDDEN_PROP, - BlockChildType, + mergeTextNodes: jest.fn((_entries: unknown[], initial: unknown) => initial), + sliceFragments: jest.fn((frags: unknown[]) => frags), }; }); @@ -67,7 +52,8 @@ jest.unstable_mockModule('../tools/ToolsManager', () => ({ })); // Now import the modules (they will receive the mocks registered above) -const { EditorJSModel, EventBus, keypath, mergeTextNodes, sliceFragments } = await import('@editorjs/model'); +const { EditorJSModel, mergeTextNodes, sliceFragments } = await import('@editorjs/model'); +const { EventBus, set, renumberKeys } = await import('@editorjs/sdk'); const ToolsManager = (await import('../tools/ToolsManager')).default; const { BlocksManager } = await import('./BlockManager.js'); @@ -333,10 +319,8 @@ describe('BlocksManager (unit, mocked deps)', () => { * Restore split-specific mock implementations that jest.resetAllMocks() clears. */ beforeEach(() => { - // @ts-expect-error — jest mock - keypath.renumberKeys.mockReturnValue(new Map()); - // @ts-expect-error — jest mock - keypath.set.mockImplementation(() => undefined); + jest.mocked(renumberKeys).mockReturnValue(new Map()); + jest.mocked(set).mockImplementation(() => undefined); // @ts-expect-error — jest mock mergeTextNodes.mockImplementation((_entries: unknown[], init: unknown) => init); // @ts-expect-error — jest mock @@ -514,20 +498,19 @@ describe('BlocksManager (unit, mocked deps)', () => { // @ts-expect-error — mock toolsManager.blockTools.get = jest.fn(() => ({ options: { canBeSplit: true } })); - // @ts-expect-error — jest mock - keypath.renumberKeys.mockReturnValue(new Map([['caption', 'caption']])); + jest.mocked(renumberKeys).mockReturnValue(new Map([['caption', 'caption']])); blocksManager.splitBlock(0, 'title' as DataKey, 3); - // keypath.set should be called for the split input and for each entry after - expect(keypath.set).toHaveBeenCalledTimes(2); - expect(keypath.set).toHaveBeenNthCalledWith( + // set should be called for the split input and for each entry after + expect(jest.mocked(set)).toHaveBeenCalledTimes(2); + expect(jest.mocked(set)).toHaveBeenNthCalledWith( 1, expect.any(Object), 'title', expect.objectContaining({ value: 'lo' }) // 'Hello'.slice(3) ); - expect(keypath.set).toHaveBeenNthCalledWith( + expect(jest.mocked(set)).toHaveBeenNthCalledWith( 2, expect.any(Object), 'caption', @@ -555,7 +538,7 @@ describe('BlocksManager (unit, mocked deps)', () => { blocksManager.splitBlock(0, 'items.0.text' as DataKey, 3); - expect(keypath.renumberKeys).toHaveBeenCalledWith(['items.0.text', 'items.1.text', 'items.2.text']); + expect(renumberKeys).toHaveBeenCalledWith(['items.0.text', 'items.1.text', 'items.2.text']); }); it('canBeSplit = true: array-indexed inputs — should use renumbered keys when setting subsequent inputs', () => { @@ -580,8 +563,7 @@ describe('BlocksManager (unit, mocked deps)', () => { toolsManager.blockTools.get = jest.fn(() => ({ options: { canBeSplit: true } })); // Simulate renumberKeys: items.1 → 0, items.2 → 1 - // @ts-expect-error — jest mock - keypath.renumberKeys.mockReturnValue( + jest.mocked(renumberKeys).mockReturnValue( new Map([ ['items.1.text', 'items.0.text'], ['items.2.text', 'items.1.text'], @@ -590,12 +572,12 @@ describe('BlocksManager (unit, mocked deps)', () => { blocksManager.splitBlock(0, 'items.0.text' as DataKey, 3); - expect(keypath.set).toHaveBeenCalledWith( + expect(jest.mocked(set)).toHaveBeenCalledWith( expect.any(Object), 'items.0.text', // renumbered from items.1.text item1Content ); - expect(keypath.set).toHaveBeenCalledWith( + expect(jest.mocked(set)).toHaveBeenCalledWith( expect.any(Object), 'items.1.text', // renumbered from items.2.text item2Content diff --git a/packages/core/src/components/BlockManager.ts b/packages/core/src/components/BlockManager.ts index 3e5abbde..6d2006c5 100644 --- a/packages/core/src/components/BlockManager.ts +++ b/packages/core/src/components/BlockManager.ts @@ -1,3 +1,4 @@ +import { EditorJSModel, mergeTextNodes, sliceFragments } from '@editorjs/model'; import { BlockIndexOrId, BlockChildType, @@ -5,13 +6,11 @@ import { type BlockNodeInit, type DataKey, type EditorDocumentSerialized, - EditorJSModel, type InlineTreeNodeSerialized, - keypath, - mergeTextNodes, NODE_TYPE_HIDDEN_PROP, - sliceFragments -} from '@editorjs/model'; + renumberKeys, + set +} from '@editorjs/sdk'; import 'reflect-metadata'; import { inject, injectable } from 'inversify'; import { TOKENS } from '../tokens.js'; @@ -357,10 +356,10 @@ export class BlocksManager { /** * In case data contains an array, we need to renumber the keys to start from 0 */ - const renumbered = keypath.renumberKeys(entriesAfter.map(([key]) => key)); + const renumbered = renumberKeys(entriesAfter.map(([key]) => key)); entriesAfter.forEach(([key, content]) => { - keypath.set(newData, renumbered.get(key) ?? key, content); + set(newData, renumbered.get(key) ?? key, content); }); } diff --git a/packages/core/src/components/BlockRenderer.spec.ts b/packages/core/src/components/BlockRenderer.spec.ts index ed4c105c..6864e323 100644 --- a/packages/core/src/components/BlockRenderer.spec.ts +++ b/packages/core/src/components/BlockRenderer.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable jsdoc/require-jsdoc, @stylistic/comma-dangle,@typescript-eslint/naming-convention */ import { beforeEach, jest } from '@jest/globals'; import type { BlockToolFacade, EditorJSAdapterPlugin } from '@editorjs/sdk'; -import type { BlockId, Index } from '@editorjs/model'; +import type { BlockId, Index } from '@editorjs/sdk'; const USER_ID = 'user'; @@ -13,39 +13,34 @@ console.error = jest.fn(); jest.unstable_mockModule('@editorjs/sdk', () => ({ BlockAddedCoreEvent: jest.fn(), BlockRemovedCoreEvent: jest.fn(), - EventBus: jest.fn(), -})); - -jest.unstable_mockModule('@editorjs/model', () => { - const EditorJSModel = jest.fn(() => ({ - addEventListener: jest.fn(), - })); - - const EventBus = jest.fn(() => ({ dispatchEvent: jest.fn() })); - - const BlockAddedEvent = function (this: { detail: unknown }, index: Index, data: unknown): void { + CoreEventType: { + Undo: 'undo', + Redo: 'redo' + }, + EventBus: jest.fn(() => ({ dispatchEvent: jest.fn() })), + BlockAddedEvent: function (this: { detail: unknown }, index: Index, data: unknown): void { this.detail = { index, data }; - }; - - const BlockRemovedEvent = function (this: { detail: unknown }, index: Index, data: unknown): void { + }, + BlockRemovedEvent: function (this: { detail: unknown }, index: Index, data: unknown): void { this.detail = { index, data }; - }; + }, + EventType: { Changed: 'changed' }, + createBlockId: (str: string) => str, +})); - const EventType = { Changed: 'changed' }; +jest.unstable_mockModule('@editorjs/model', () => { + const EditorJSModel = jest.fn(() => ({ + addEventListener: jest.fn(), + })); return { EditorJSModel, - EventBus, - BlockAddedEvent, - BlockRemovedEvent, - EventType, - createBlockId: (str: string) => str, }; }); @@ -61,7 +56,8 @@ jest.unstable_mockModule('../tools/ToolsManager', () => ({ })); // Now import the modules (they will receive the mocks registered above) -const { EditorJSModel, EventBus, BlockAddedEvent, BlockRemovedEvent } = await import('@editorjs/model'); +const { EditorJSModel } = await import('@editorjs/model'); +const { BlockAddedEvent, BlockRemovedEvent, EventBus } = await import('@editorjs/sdk'); const ToolsManager = (await import('../tools/ToolsManager')).default; const { BlockRenderer } = await import('./BlockRenderer.js'); diff --git a/packages/core/src/components/BlockRenderer.ts b/packages/core/src/components/BlockRenderer.ts index fabd6b7b..479ea086 100644 --- a/packages/core/src/components/BlockRenderer.ts +++ b/packages/core/src/components/BlockRenderer.ts @@ -1,11 +1,11 @@ +import { EditorJSModel } from '@editorjs/model'; import { BlockAddedEvent, BlockRemovedEvent, createBlockId, - EditorJSModel, EventType, ModelEvents -} from '@editorjs/model'; +} from '@editorjs/sdk'; import 'reflect-metadata'; import { inject, injectable } from 'inversify'; import { TOKENS } from '../tokens.js'; diff --git a/packages/core/src/components/SelectionManager.spec.ts b/packages/core/src/components/SelectionManager.spec.ts index 4f718074..1758646e 100644 --- a/packages/core/src/components/SelectionManager.spec.ts +++ b/packages/core/src/components/SelectionManager.spec.ts @@ -1,23 +1,13 @@ /* eslint-disable @typescript-eslint/no-magic-numbers, jsdoc/require-jsdoc,@typescript-eslint/naming-convention */ import { jest } from '@jest/globals'; -import type { CoreConfigValidated } from '@editorjs/sdk'; -// @ts-expect-error - TS don't import types via import() so have to import them here as well -import type { CaretManagerEvents, InlineFragment, InlineToolName, EventType, Index } from '@editorjs/model'; +import type { CoreConfigValidated, CaretManagerEvents, InlineToolName } from '@editorjs/sdk'; +import type { InlineFragment } from '@editorjs/sdk'; +// @ts-expect-error -- type imports +import type { EventType, Index } from '@editorjs/sdk'; // Register ESM mocks before importing the module under test jest.unstable_mockModule('@editorjs/model', () => { - const caretManagerCaretUpdatedEvent = function ( - this: { detail: Record }, - detail: Record - ): void { - this.detail = detail; - }; - - const eventType: Record = {}; - - eventType.CaretManagerUpdated = 'caret-updated'; - const EditorJSModel = jest.fn(() => ({ addEventListener: jest.fn(), getFragments: jest.fn(() => []), @@ -29,13 +19,6 @@ jest.unstable_mockModule('@editorjs/model', () => { return { EditorJSModel, - CaretManagerCaretUpdatedEvent: caretManagerCaretUpdatedEvent, - Index: { parse: jest.fn() }, - EventType: eventType, - createInlineToolData: (data: Record) => data, - createInlineToolName: (name: string) => name, - FormattingAction: { Format: 'format', - Unformat: 'unformat' }, }; }); @@ -46,6 +29,20 @@ jest.unstable_mockModule('@editorjs/sdk', () => ({ }), EventBus: jest.fn(() => ({ dispatchEvent: jest.fn() })), IndexError: class IndexError extends Error {}, + CaretManagerCaretUpdatedEvent: function ( + this: { detail: Record }, + detail: Record + ): void { + this.detail = detail; + }, + Index: { parse: jest.fn() }, + EventType: { CaretManagerUpdated: 'caret-updated' }, + createInlineToolData: (data: Record) => data, + createInlineToolName: (name: string) => name, + FormattingAction: { + Format: 'format', + Unformat: 'unformat', + }, })); jest.unstable_mockModule('../tools/ToolsManager', () => ({ @@ -54,8 +51,8 @@ jest.unstable_mockModule('../tools/ToolsManager', () => ({ })), })); -const { EditorJSModel, EventType, CaretManagerCaretUpdatedEvent, Index } = await import('@editorjs/model'); -const { SelectionChangedCoreEvent, EventBus } = await import('@editorjs/sdk'); +const { EditorJSModel } = await import('@editorjs/model'); +const { CaretManagerCaretUpdatedEvent, EventType, Index, SelectionChangedCoreEvent, EventBus } = await import('@editorjs/sdk'); const ToolsManager = (await import('../tools/ToolsManager')).default; const { SelectionManager } = await import('./SelectionManager.js'); diff --git a/packages/core/src/components/SelectionManager.ts b/packages/core/src/components/SelectionManager.ts index 6f2c1ad5..fae957d0 100644 --- a/packages/core/src/components/SelectionManager.ts +++ b/packages/core/src/components/SelectionManager.ts @@ -1,20 +1,23 @@ import 'reflect-metadata'; +import { EditorJSModel } from '@editorjs/model'; +import type { InlineFragment } from '@editorjs/sdk'; import { + CaretManagerCaretUpdatedEvent, CaretManagerEvents, + CoreConfigValidated, createInlineToolData, - FormattingAction, - InlineFragment, - InlineToolName -} from '@editorjs/model'; -import { CaretManagerCaretUpdatedEvent, Index, EditorJSModel, createInlineToolName } from '@editorjs/model'; -import { EventType } from '@editorjs/model'; -import { + createInlineToolName, EventBus, - SelectionChangedCoreEvent, IndexError, CoreConfigValidated + EventType, + FormattingAction, + Index, + IndexError, + InlineToolFormatData, + InlineToolName, + SelectionChangedCoreEvent } from '@editorjs/sdk'; import { inject, injectable } from 'inversify'; import { TOKENS } from '../tokens.js'; -import { InlineToolFormatData } from '@editorjs/sdk'; import ToolsManager from '../tools/ToolsManager.js'; /** diff --git a/packages/core/src/components/UndoRedoManager.spec.ts b/packages/core/src/components/UndoRedoManager.spec.ts index d4d08d22..3fc2a56d 100644 --- a/packages/core/src/components/UndoRedoManager.spec.ts +++ b/packages/core/src/components/UndoRedoManager.spec.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-magic-numbers, jsdoc/require-jsdoc, @typescript-eslint/naming-convention */ import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'; import type { CoreConfigValidated } from '@editorjs/sdk'; -// @ts-expect-error -- jest module mock doesn't import type -import type { EventType } from '@editorjs/model'; +// @ts-expect-error -- type import +import type { EventType } from '@editorjs/sdk'; const USER_ID = 'user'; const OTHER_USER_ID = 'other-user'; @@ -17,18 +17,8 @@ jest.unstable_mockModule('@editorjs/model', () => { modifyData: jest.fn(), })); - const EventType = { Changed: 'model:changed' }; - - const EventAction = { - Added: 'added', - Removed: 'removed', - Modified: 'modified', - }; - return { EditorJSModel, - EventType, - EventAction, }; }); @@ -42,10 +32,16 @@ jest.unstable_mockModule('@editorjs/sdk', () => ({ removeEventListener: jest.fn(), dispatchEvent: jest.fn(), })), + EventType: { Changed: 'model:changed' }, + EventAction: { + Added: 'added', + Removed: 'removed', + Modified: 'modified', + }, })); -const { EditorJSModel, EventType, EventAction } = await import('@editorjs/model'); -const { EventBus } = await import('@editorjs/sdk'); +const { EditorJSModel } = await import('@editorjs/model'); +const { EventType, EventAction, EventBus } = await import('@editorjs/sdk'); const { UndoRedoManager } = await import('./UndoRedoManager.js'); // ─── helpers ──────────────────────────────────────────────────────────────── diff --git a/packages/core/src/components/UndoRedoManager.ts b/packages/core/src/components/UndoRedoManager.ts index 6f3c9811..0fba6c66 100644 --- a/packages/core/src/components/UndoRedoManager.ts +++ b/packages/core/src/components/UndoRedoManager.ts @@ -1,15 +1,19 @@ import 'reflect-metadata'; import { inject, injectable } from 'inversify'; +import { EditorJSModel } from '@editorjs/model'; import { - EditorJSModel, + CoreConfigValidated, + CoreEventType, EventAction, + EventBus, EventPayloadBase, EventType, type ModelEvents, - ModifiedEventData -} from '@editorjs/model'; -import { type CoreConfigValidated, CoreEventType, EventBus, RedoCoreEvent, UndoCoreEvent } from '@editorjs/sdk'; + ModifiedEventData, + RedoCoreEvent, + UndoCoreEvent +} from '@editorjs/sdk'; import { TOKENS } from '../tokens.js'; /** diff --git a/packages/core/src/tools/internal/block-tools/paragraph/index.ts b/packages/core/src/tools/internal/block-tools/paragraph/index.ts index 1819dfc1..e9f56cb3 100644 --- a/packages/core/src/tools/internal/block-tools/paragraph/index.ts +++ b/packages/core/src/tools/internal/block-tools/paragraph/index.ts @@ -1,11 +1,11 @@ import type { ToolConfig } from '@editorjs/editorjs'; -import type { TextNodeSerialized } from '@editorjs/model'; import type { BlockTool, BlockToolConstructor, BlockToolConstructorOptions, BlockToolData, - KeyAddedEvent + KeyAddedEvent, + TextNodeSerialized } from '@editorjs/sdk'; import { KeyRemovedEvent } from '@editorjs/sdk'; import { ToolType } from '@editorjs/sdk'; diff --git a/packages/core/src/tools/internal/inline-tools/bold/index.ts b/packages/core/src/tools/internal/inline-tools/bold/index.ts index ba3ec7bf..88cc4a8e 100644 --- a/packages/core/src/tools/internal/inline-tools/bold/index.ts +++ b/packages/core/src/tools/internal/inline-tools/bold/index.ts @@ -1,8 +1,5 @@ -import type { ToolFormattingOptions, InlineTool, InlineToolConstructor } from '@editorjs/sdk'; -import { ToolType } from '@editorjs/sdk'; -import type { InlineFragment, TextRange } from '@editorjs/model'; -import { FormattingAction } from '@editorjs/model'; -import { IntersectType } from '@editorjs/model'; +import type { ToolFormattingOptions, InlineTool, InlineToolConstructor, TextRange, InlineFragment } from '@editorjs/sdk'; +import { ToolType, FormattingAction, IntersectType } from '@editorjs/sdk'; import { make } from '@editorjs/dom'; /** diff --git a/packages/core/src/tools/internal/inline-tools/italic/index.ts b/packages/core/src/tools/internal/inline-tools/italic/index.ts index 00d848b5..96324098 100644 --- a/packages/core/src/tools/internal/inline-tools/italic/index.ts +++ b/packages/core/src/tools/internal/inline-tools/italic/index.ts @@ -1,8 +1,5 @@ -import type { ToolFormattingOptions, InlineTool, InlineToolConstructor } from '@editorjs/sdk'; -import { ToolType } from '@editorjs/sdk'; -import type { InlineFragment, TextRange } from '@editorjs/model'; -import { FormattingAction } from '@editorjs/model'; -import { IntersectType } from '@editorjs/model'; +import type { ToolFormattingOptions, InlineTool, InlineToolConstructor, TextRange, InlineFragment } from '@editorjs/sdk'; +import { ToolType, FormattingAction, IntersectType } from '@editorjs/sdk'; import { make } from '@editorjs/dom'; /** diff --git a/packages/core/src/tools/internal/inline-tools/link/index.ts b/packages/core/src/tools/internal/inline-tools/link/index.ts index 57d15c29..8fa7b249 100644 --- a/packages/core/src/tools/internal/inline-tools/link/index.ts +++ b/packages/core/src/tools/internal/inline-tools/link/index.ts @@ -2,14 +2,16 @@ import type { ActionsElementWithOptions, ToolFormattingOptions, InlineTool, - InlineToolFormatData, InlineToolConstructor + InlineToolFormatData, + InlineToolConstructor, + TextRange, + InlineFragment } from '@editorjs/sdk'; import { - ToolType + ToolType, + FormattingAction, + IntersectType } from '@editorjs/sdk'; -import type { InlineFragment, TextRange } from '@editorjs/model'; -import { FormattingAction } from '@editorjs/model'; -import { IntersectType } from '@editorjs/model'; import { make } from '@editorjs/dom'; /** diff --git a/packages/core/src/utils/composeDataFromVersion2.ts b/packages/core/src/utils/composeDataFromVersion2.ts index c6d8dcb3..201c81b5 100644 --- a/packages/core/src/utils/composeDataFromVersion2.ts +++ b/packages/core/src/utils/composeDataFromVersion2.ts @@ -1,6 +1,7 @@ import type { OutputData } from '@editorjs/editorjs'; -import type { InlineFragment } from '@editorjs/model'; -import { createInlineToolData, createInlineToolName, TextNode, ValueNode, type BlockNodeInit } from '@editorjs/model'; +import { TextNode, ValueNode } from '@editorjs/model'; +import type { InlineFragment } from '@editorjs/sdk'; +import { createInlineToolData, createInlineToolName, type BlockNodeInit } from '@editorjs/sdk'; /** * Removes HTML tags from the input string diff --git a/packages/dom-adapters/package.json b/packages/dom-adapters/package.json index e5138dfa..779e46ab 100644 --- a/packages/dom-adapters/package.json +++ b/packages/dom-adapters/package.json @@ -23,11 +23,11 @@ "@stryker-mutator/jest-runner": "^7.0.2", "@stryker-mutator/typescript-checker": "^7.0.2", "@types/eslint": "^8", - "@types/jest": "^29.5.1", + "@types/jest": "^30.0.0", "eslint": "^8.38.0", "eslint-config-codex": "^2.0.3", "eslint-plugin-import": "^2.29.0", - "jest": "^29.7.0", + "jest": "^30.4.2", "stryker-cli": "^1.0.2", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", @@ -35,7 +35,6 @@ }, "dependencies": { "@editorjs/dom": "^1.1.0", - "@editorjs/model": "workspace:^", "@editorjs/sdk": "workspace:^", "inversify": "^8.1.0", "reflect-metadata": "^0.2.2" diff --git a/packages/dom-adapters/src/BlockToolAdapter/EventTarget.d.ts b/packages/dom-adapters/src/BlockToolAdapter/EventTarget.d.ts index b933e02c..0519c5b4 100644 --- a/packages/dom-adapters/src/BlockToolAdapter/EventTarget.d.ts +++ b/packages/dom-adapters/src/BlockToolAdapter/EventTarget.d.ts @@ -1,4 +1,4 @@ -import type { EventMap } from '@editorjs/model'; +import type { EventMap } from '@editorjs/sdk'; /** * Augment EventTarget's addEventListener method to accept CustomEvent diff --git a/packages/dom-adapters/src/BlockToolAdapter/index.ts b/packages/dom-adapters/src/BlockToolAdapter/index.ts index 4c231361..b455f351 100644 --- a/packages/dom-adapters/src/BlockToolAdapter/index.ts +++ b/packages/dom-adapters/src/BlockToolAdapter/index.ts @@ -5,7 +5,7 @@ import { type ModelEvents, TextAddedEvent, TextRemovedEvent -} from '@editorjs/model'; +} from '@editorjs/sdk'; import type { BeforeInputUIEvent, BeforeInputUIEventPayload, diff --git a/packages/dom-adapters/src/CaretAdapter/index.ts b/packages/dom-adapters/src/CaretAdapter/index.ts index 49a24cc7..c3f2d780 100644 --- a/packages/dom-adapters/src/CaretAdapter/index.ts +++ b/packages/dom-adapters/src/CaretAdapter/index.ts @@ -6,7 +6,7 @@ import { IndexBuilder, createDataKey, createBlockId -} from '@editorjs/model'; +} from '@editorjs/sdk'; import type { CoreConfig, EditorAPI } from '@editorjs/sdk'; import { getAbsoluteRangeOffset, diff --git a/packages/dom-adapters/src/FormattingAdapter/index.ts b/packages/dom-adapters/src/FormattingAdapter/index.ts index eb2e91d1..a23563b7 100644 --- a/packages/dom-adapters/src/FormattingAdapter/index.ts +++ b/packages/dom-adapters/src/FormattingAdapter/index.ts @@ -2,14 +2,14 @@ import type { InlineFragment, InlineToolName, ModelEvents -} from '@editorjs/model'; +} from '@editorjs/sdk'; import { createInlineToolName -} from '@editorjs/model'; +} from '@editorjs/sdk'; import { TextFormattedEvent, TextUnformattedEvent -} from '@editorjs/model'; +} from '@editorjs/sdk'; import { CaretAdapter } from '../CaretAdapter/index.js'; import type { CoreConfig, EditorAPI, InlineTool, ToolLoadedCoreEvent } from '@editorjs/sdk'; import { CoreEventType, EventBus } from '@editorjs/sdk'; diff --git a/packages/dom-adapters/src/InputsRegistry/index.ts b/packages/dom-adapters/src/InputsRegistry/index.ts index 2fe90d89..8e03325a 100644 --- a/packages/dom-adapters/src/InputsRegistry/index.ts +++ b/packages/dom-adapters/src/InputsRegistry/index.ts @@ -1,4 +1,4 @@ -import type { BlockId, DataKey } from '@editorjs/model'; +import type { BlockId, DataKey } from '@editorjs/sdk'; import { injectable } from 'inversify'; /** diff --git a/packages/dom-adapters/src/index.ts b/packages/dom-adapters/src/index.ts index 398ad067..683fa223 100644 --- a/packages/dom-adapters/src/index.ts +++ b/packages/dom-adapters/src/index.ts @@ -9,7 +9,7 @@ import { EventBus } from '@editorjs/sdk'; import { PluginType } from '@editorjs/sdk'; import { DOMBlockToolAdapter } from './BlockToolAdapter/index.js'; import { InputsRegistry } from './InputsRegistry/index.js'; -import type { BlockId } from '@editorjs/model'; +import type { BlockId } from '@editorjs/sdk'; import { Container } from 'inversify'; import { TOKENS } from './tokens.js'; import type { CoreConfig } from '@editorjs/sdk'; diff --git a/packages/dom-adapters/src/utils/doRangesIntersect.ts b/packages/dom-adapters/src/utils/doRangesIntersect.ts index 02d05f6b..68a4d73b 100644 --- a/packages/dom-adapters/src/utils/doRangesIntersect.ts +++ b/packages/dom-adapters/src/utils/doRangesIntersect.ts @@ -1,4 +1,4 @@ -import type { TextRange } from '@editorjs/model'; +import type { TextRange } from '@editorjs/sdk'; /** * Check if two ranges have intersection diff --git a/packages/dom-adapters/src/utils/mergeTextRanges.ts b/packages/dom-adapters/src/utils/mergeTextRanges.ts index a03406e9..93ab455b 100644 --- a/packages/dom-adapters/src/utils/mergeTextRanges.ts +++ b/packages/dom-adapters/src/utils/mergeTextRanges.ts @@ -1,4 +1,4 @@ -import type { TextRange } from '@editorjs/model'; +import type { TextRange } from '@editorjs/sdk'; import { doRangesIntersect } from './doRangesIntersect.js'; /** diff --git a/packages/dom-adapters/src/utils/selectionRangeInInput.ts b/packages/dom-adapters/src/utils/selectionRangeInInput.ts index b58ea5a0..fe5ff63e 100644 --- a/packages/dom-adapters/src/utils/selectionRangeInInput.ts +++ b/packages/dom-adapters/src/utils/selectionRangeInInput.ts @@ -1,4 +1,4 @@ -import type { TextRange } from '@editorjs/model'; +import type { TextRange } from '@editorjs/sdk'; import { getAbsoluteRangeOffset } from './getAbsoluteRangeOffset.js'; /** diff --git a/packages/dom-adapters/src/utils/surround.ts b/packages/dom-adapters/src/utils/surround.ts index a2a887d6..888fcf0c 100644 --- a/packages/dom-adapters/src/utils/surround.ts +++ b/packages/dom-adapters/src/utils/surround.ts @@ -1,4 +1,4 @@ -import type { TextRange } from '@editorjs/model'; +import type { TextRange } from '@editorjs/sdk'; import { getBoundaryPointByAbsoluteOffset } from './getRelativeIndex.js'; /** diff --git a/packages/model-types/.gitignore b/packages/model-types/.gitignore new file mode 100644 index 00000000..854a697d --- /dev/null +++ b/packages/model-types/.gitignore @@ -0,0 +1,24 @@ +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Swap the comments on the following lines if you don't wish to use zero-installs +# Documentation here: https://yarnpkg.com/features/zero-installs +#!.yarn/cache +#.pnp.* + +# IDE +.idea/* + +node_modules/* +dist/* + +# tests +coverage/ +reports/ + +# stryker temp files +.stryker-tmp diff --git a/packages/model-types/eslint.config.mjs b/packages/model-types/eslint.config.mjs new file mode 100644 index 00000000..57863c88 --- /dev/null +++ b/packages/model-types/eslint.config.mjs @@ -0,0 +1,35 @@ +import CodeX from 'eslint-config-codex'; + +export default [ + ...CodeX, + { + languageOptions: { + parserOptions: { + project: './tsconfig.eslint.json', + tsconfigRootDir: import.meta.dirname, + sourceType: 'module', + }, + }, + rules: { + 'n/no-unpublished-import': ['error', { + allowModules: [ + 'eslint-config-codex', + ], + ignoreTypeImport: true, + }], + 'n/no-missing-import': 'off', + 'n/no-unsupported-features/node-builtins': ['error', { + version: '>=24.0.0', + ignores: [], + }], + }, + }, + { + files: ['**/*.spec.ts'], + rules: { + 'n/no-unpublished-import': ['error', { + allowModules: ['@jest/globals', 'ts-jest'], + }], + }, + }, +]; diff --git a/packages/model-types/jest.config.ts b/packages/model-types/jest.config.ts new file mode 100644 index 00000000..bfc155ab --- /dev/null +++ b/packages/model-types/jest.config.ts @@ -0,0 +1,22 @@ +import type { JestConfigWithTsJest } from 'ts-jest'; + +export default { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['/src/**/*.spec.ts'], + extensionsToTreatAsEsm: ['.ts'], + moduleNameMapper: { + '^@editorjs/model-types$': '/src/index.ts', + '^@editorjs/model-types/(.*)$': '/src/$1', + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + coverageReporters: ['lcov', 'json-summary', 'text-summary'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + useESM: true, + }, + ], + }, +} as JestConfigWithTsJest; diff --git a/packages/model-types/package.json b/packages/model-types/package.json new file mode 100644 index 00000000..f2c98d10 --- /dev/null +++ b/packages/model-types/package.json @@ -0,0 +1,30 @@ +{ + "name": "@editorjs/model-types", + "version": "0.0.0", + "packageManager": "yarn@4.0.1", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build": "tsc --project tsconfig.build.json", + "build:declaration": "yarn build --emitDeclarationOnly", + "lint": "eslint ./src", + "lint:ci": "yarn lint --max-warnings 0", + "lint:fix": "yarn lint --fix", + "test": "jest", + "test:coverage": "yarn test --coverage=true", + "clear": "rm -rf ./dist && rm -rf ./tsconfig.build.tsbuildinfo" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@types/eslint": "^9.6.1", + "@types/jest": "^30.0.0", + "eslint": "^9.24.0", + "eslint-config-codex": "^2.0.3", + "eslint-plugin-import": "^2.31.0", + "jest": "^30.4.2", + "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", + "typescript": "^5.5.4" + } +} diff --git a/packages/model/src/EventBus/events/BaseEvent.ts b/packages/model-types/src/BaseDocumentEvent.ts similarity index 63% rename from packages/model/src/EventBus/events/BaseEvent.ts rename to packages/model-types/src/BaseDocumentEvent.ts index 30818dd7..77050290 100644 --- a/packages/model/src/EventBus/events/BaseEvent.ts +++ b/packages/model-types/src/BaseDocumentEvent.ts @@ -1,6 +1,9 @@ -import type { Index } from '../../entities/Index/index.js'; -import type { EventAction } from '../types/EventAction.js'; -import { EventType } from '../types/EventType.js'; +import type { EventAction } from './EventAction.js'; +import { EventType } from './EventType.js'; +import type { Index } from './Index/index.js'; +import type { ModifiedEventData } from './EventBus.js'; + +export type { ModifiedEventData }; /** * Common fields for all events related to the document model @@ -27,14 +30,6 @@ export interface EventPayloadBase { userId: number | string; } -/** - * Base data interface for Modified event with new and previous values - */ -export interface ModifiedEventData { - value: T; - previous: T; -} - /** * BaseDocumentEvent Custom Event */ @@ -42,12 +37,11 @@ export class BaseDocumentEvent exten /** * BaseDocumentEvent class constructor * @param index - index of the modified value in the document - * @param action - event action - * @param data - event data + * @param action - the action that was performed + * @param data - payload describing the change * @param userId - user identifier */ constructor(index: Index, action: Action, data: Data, userId: string | number) { - /* Stryker disable next-line ObjectLiteral -- keep detail payload intact; mutants yield null/malformed detail and crash listeners (event.detail.index) */ super(EventType.Changed, { detail: { index, diff --git a/packages/model-types/src/BlockChildType.ts b/packages/model-types/src/BlockChildType.ts new file mode 100644 index 00000000..578b0e9e --- /dev/null +++ b/packages/model-types/src/BlockChildType.ts @@ -0,0 +1,10 @@ +/** Hidden property name used to mark block child nodes */ +export const NODE_TYPE_HIDDEN_PROP = '$t'; + +/** Describes the type of a block child node */ +export enum BlockChildType { + /** Node stores a value */ + Value = 'v', + /** Node stores text with inline formatting */ + Text = 't' +} diff --git a/packages/model-types/src/BlockId.ts b/packages/model-types/src/BlockId.ts new file mode 100644 index 00000000..f7bfb71e --- /dev/null +++ b/packages/model-types/src/BlockId.ts @@ -0,0 +1,35 @@ +import { create, type Nominal } from './Nominal.js'; + +/** Unique identifier for a block */ +export type BlockId = Nominal; + +/** Union representing either a numeric index or a block identifier */ +export type BlockIndexOrId = number | BlockId; + +/** Name of a block tool */ +export type BlockToolName = Nominal; + +/** Factory for BlockToolName */ +export const createBlockToolName = create(); + +/** Function returns a value with the nominal BlockId type */ +export const createBlockId = create(); + +/** + * URL-safe alphabet used for ID generation + */ +const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'; + +const ID_LENGTH = 21; + +/** + * Bitmask selecting a character from the 64-character ALPHABET via a single random byte + */ +const ALPHABET_MASK = ALPHABET.length - 1; + +/** Generates a new unique BlockId */ +export const generateBlockId = (): BlockId => { + const bytes = crypto.getRandomValues(new Uint8Array(ID_LENGTH)); + + return createBlockId(Array.from(bytes, byte => ALPHABET[byte & ALPHABET_MASK]).join('')); +}; diff --git a/packages/model-types/src/BlockNode.ts b/packages/model-types/src/BlockNode.ts new file mode 100644 index 00000000..d4fcf069 --- /dev/null +++ b/packages/model-types/src/BlockNode.ts @@ -0,0 +1,38 @@ +import type { TextNodeSerialized } from './Text.js'; +import type { ValueSerialized } from './Value.js'; +import type { BlockTuneSerialized } from './BlockTune.js'; + +// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents +export type BlockChildNodeSerialized = ValueSerialized | TextNodeSerialized; + +// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents +export type BlockNodeDataSerializedValue = BlockChildNodeSerialized | BlockChildNodeSerialized[] | BlockNodeDataSerialized | BlockNodeDataSerialized[]; + +/** Record mapping data keys to serialized block child values */ +export interface BlockNodeDataSerialized { + [key: string]: BlockNodeDataSerializedValue; +} + +/** Describes a block with its id, tool name, data and optional tunes */ +export interface BlockData { + /** Block identifier */ + id: string; + /** Tool name used to render the block */ + name: string; + /** Block content data */ + data: BlockNodeDataSerialized; + /** Optional block tune data */ + tunes?: Record; +} + +/** Minimum data required to initialise a block */ +export interface BlockNodeInitBase { + /** Tool name to use for the block */ + name: string; +} + +/** Partial block data used for initialisation */ +export type BlockNodeInit = BlockNodeInitBase & Partial>; + +/** Serialized version of a block node (alias for BlockData) */ +export type BlockNodeSerialized = BlockData; diff --git a/packages/model-types/src/BlockTune.ts b/packages/model-types/src/BlockTune.ts new file mode 100644 index 00000000..0da8c3db --- /dev/null +++ b/packages/model-types/src/BlockTune.ts @@ -0,0 +1,13 @@ +import type { Nominal } from './Nominal.js'; +import { create } from './Nominal.js'; + +/** Name of a block tune */ +export type BlockTuneName = Nominal; + +/** Factory for BlockTuneName */ +export const createBlockTuneName = create(); + +/** Serialized data for a block tune */ +export interface BlockTuneSerialized { + [key: string]: unknown; +} diff --git a/packages/model-types/src/Caret.ts b/packages/model-types/src/Caret.ts new file mode 100644 index 00000000..bd0c8b78 --- /dev/null +++ b/packages/model-types/src/Caret.ts @@ -0,0 +1,15 @@ +import type { Index } from './Index/index.js'; + +/** + * Caret is responsible for storing caret index + */ +export interface Caret { + /** Caret index */ + readonly index: Index | null; + + /** User identifier */ + readonly userId: string | number; + + /** Updates caret index */ + update(index: Index | null): void; +} diff --git a/packages/model-types/src/ChangeData.ts b/packages/model-types/src/ChangeData.ts new file mode 100644 index 00000000..9e1c1dd7 --- /dev/null +++ b/packages/model-types/src/ChangeData.ts @@ -0,0 +1,7 @@ +import type { DocumentData } from './EditorDocument.js'; + +/** Payload for document change events containing the full document snapshot */ +export interface ChangeData { + /** Updated document data */ + data: DocumentData; +} diff --git a/packages/model-types/src/DataKey.ts b/packages/model-types/src/DataKey.ts new file mode 100644 index 00000000..af31a4c2 --- /dev/null +++ b/packages/model-types/src/DataKey.ts @@ -0,0 +1,7 @@ +import { create, type Nominal } from './Nominal.js'; + +/** Key used for block data properties */ +export type DataKey = Nominal; + +/** Function returns a value with the nominal DataKey type */ +export const createDataKey = create(); diff --git a/packages/model-types/src/EditorDocument.ts b/packages/model-types/src/EditorDocument.ts new file mode 100644 index 00000000..638516db --- /dev/null +++ b/packages/model-types/src/EditorDocument.ts @@ -0,0 +1,24 @@ +import type { BlockData } from './BlockNode.js'; + +/** Free-form properties that can be attached to a document */ +export type Properties = Record; + +/** Represents the full document with its blocks and optional properties */ +export interface DocumentData { + /** Document identifier */ + identifier: string; + /** List of blocks in the document */ + blocks: BlockData[]; + /** Document-level properties */ + properties: Properties; +} + +/** Serialized representation of an EditorDocument */ +export interface EditorDocumentSerialized { + /** Document identifier */ + identifier: string; + /** Array of serialized block nodes */ + blocks: BlockData[]; + /** Document-level properties */ + properties: Properties; +} diff --git a/packages/model/src/EventBus/types/EventAction.ts b/packages/model-types/src/EventAction.ts similarity index 100% rename from packages/model/src/EventBus/types/EventAction.ts rename to packages/model-types/src/EventAction.ts diff --git a/packages/model-types/src/EventBus.ts b/packages/model-types/src/EventBus.ts new file mode 100644 index 00000000..d152c886 --- /dev/null +++ b/packages/model-types/src/EventBus.ts @@ -0,0 +1,19 @@ +/** + * Generic event bus used across the document model + */ +export class EventBus extends EventTarget {} + +/** + * Base data interface for Modified event with new and previous values + */ +export interface ModifiedEventData { + /** + * New value + */ + value: T; + + /** + * Previous value + */ + previous: T; +} diff --git a/packages/model-types/src/EventMap.ts b/packages/model-types/src/EventMap.ts new file mode 100644 index 00000000..095fdf0b --- /dev/null +++ b/packages/model-types/src/EventMap.ts @@ -0,0 +1,47 @@ +import type { BlockAddedEvent } from './events/BlockAddedEvent.js'; +import type { BlockRemovedEvent } from './events/BlockRemovedEvent.js'; +import type { TextAddedEvent } from './events/TextAddedEvent.js'; +import type { TextRemovedEvent } from './events/TextRemovedEvent.js'; +import type { TextFormattedEvent } from './events/TextFormattedEvent.js'; +import type { TextUnformattedEvent } from './events/TextUnformattedEvent.js'; +import type { ValueModifiedEvent } from './events/ValueModifiedEvent.js'; +import type { TuneModifiedEvent } from './events/TuneModifiedEvent.js'; +import type { PropertyModifiedEvent } from './events/PropertyModifiedEvent.js'; +import type { CaretManagerCaretAddedEvent } from './events/CaretManagerCaretAddedEvent.js'; +import type { CaretManagerCaretRemovedEvent } from './events/CaretManagerCaretRemovedEvent.js'; +import type { CaretManagerCaretUpdatedEvent } from './events/CaretManagerCaretUpdatedEvent.js'; + +/** + * Alias for all block events + */ +export type BlockEvents = BlockAddedEvent | BlockRemovedEvent; + +/** + * Alias for all text node events + */ +export type TextNodeEvents = TextAddedEvent | TextRemovedEvent | TextFormattedEvent | TextUnformattedEvent; + +/** + * Alias for all value node events + */ +export type ValueNodeEvents = ValueModifiedEvent; + +/** + * Alias for all block tune events + */ +export type BlockTuneEvents = TuneModifiedEvent; + +/** + * Alias for all document-level events + */ +export type DocumentEvents = PropertyModifiedEvent; + +/** + * Union of all events that can be emitted by the document model + */ +export type ModelEvents = BlockEvents | TextNodeEvents | ValueNodeEvents | BlockTuneEvents | DocumentEvents; + +/** + * Union of all events that can be emitted by the caret manager + */ +export type CaretManagerEvents = CaretManagerCaretAddedEvent | CaretManagerCaretUpdatedEvent | CaretManagerCaretRemovedEvent; diff --git a/packages/model/src/EventBus/types/EventType.ts b/packages/model-types/src/EventType.ts similarity index 100% rename from packages/model/src/EventBus/types/EventType.ts rename to packages/model-types/src/EventType.ts diff --git a/packages/model-types/src/FormattingAction.ts b/packages/model-types/src/FormattingAction.ts new file mode 100644 index 00000000..c9e9cd13 --- /dev/null +++ b/packages/model-types/src/FormattingAction.ts @@ -0,0 +1,7 @@ +/** Describes formatting action type */ +export enum FormattingAction { + /** Apply formatting */ + Format = 'format', + /** Remove formatting */ + Unformat = 'unformat' +} diff --git a/packages/model/src/entities/Index/Index.spec.ts b/packages/model-types/src/Index/Index.spec.ts similarity index 98% rename from packages/model/src/entities/Index/Index.spec.ts rename to packages/model-types/src/Index/Index.spec.ts index 52f4c252..657de9c5 100644 --- a/packages/model/src/entities/Index/Index.spec.ts +++ b/packages/model-types/src/Index/Index.spec.ts @@ -1,7 +1,5 @@ -import type { DocumentIndex } from '../../EventBus/index.js'; -import type { DataKey } from '../BlockNode/index.js'; -import type { BlockTuneName } from '../BlockTune/index.js'; -import { IndexBuilder } from '../index.js'; +import type { DataKey, DocumentIndex, BlockTuneName } from '@editorjs/model-types'; +import { IndexBuilder } from './IndexBuilder.js'; import { Index } from './index.js'; describe('Index', () => { diff --git a/packages/model/src/entities/Index/IndexBuilder.spec.ts b/packages/model-types/src/Index/IndexBuilder.spec.ts similarity index 93% rename from packages/model/src/entities/Index/IndexBuilder.spec.ts rename to packages/model-types/src/Index/IndexBuilder.spec.ts index 0ec45840..fcceade5 100644 --- a/packages/model/src/entities/Index/IndexBuilder.spec.ts +++ b/packages/model-types/src/Index/IndexBuilder.spec.ts @@ -1,6 +1,4 @@ -import type { DocumentIndex } from '../../EventBus/index.js'; -import type { DataKey } from '../BlockNode/index.js'; -import type { BlockTuneName } from '../BlockTune/index.js'; +import type { DataKey, DocumentIndex, BlockTuneName } from '@editorjs/model-types'; import { IndexBuilder } from './IndexBuilder.js'; describe('IndexBuilder', () => { diff --git a/packages/model-types/src/Index/IndexBuilder.ts b/packages/model-types/src/Index/IndexBuilder.ts new file mode 100644 index 00000000..1cf0b345 --- /dev/null +++ b/packages/model-types/src/Index/IndexBuilder.ts @@ -0,0 +1,117 @@ +import type { BlockTuneName } from '../BlockTune.js'; +import type { DataKey } from '../DataKey.js'; +import type { DocumentId } from '../indexing.js'; +import type { TextRange } from '../Text.js'; +import { Index } from './index.js'; + +/** + * Builder for the Index class + */ +export class IndexBuilder { + #index = new Index(); + + /** + * Sets the character range the index points to within a text data property + * @param range - start/end offsets of the text range + */ + public addTextRange(range?: TextRange): this { + this.#index.textRange = range; + + return this; + } + + /** + * Sets the data property the index points to within a block node + * @param key - identifier of the data property + */ + public addDataKey(key?: DataKey): this { + this.#index.dataKey = key; + + return this; + } + + /** + * Sets the tune-specific property the index points to + * @param key - identifier of the tune property + */ + public addTuneKey(key?: string): this { + this.#index.tuneKey = key; + + return this; + } + + /** + * Sets the block tune the index points to + * @param name - identifier of the tune + */ + public addTuneName(name?: BlockTuneName): this { + this.#index.tuneName = name; + + return this; + } + + /** + * Sets the position of the block node the index points to + * @param index - zero-based position of the block within the document + */ + public addBlockIndex(index?: number): this { + this.#index.blockIndex = index; + + return this; + } + + /** + * Sets the document-level property the index points to + * @param name - identifier of the property + */ + public addPropertyName(name?: string): this { + this.#index.propertyName = name; + + return this; + } + + /** + * Sets the document the index points to + * @param id - identifier of the document + */ + public addDocumentId(id?: DocumentId): this { + this.#index.documentId = id; + + return this; + } + + /** + * Validates the accumulated fields and returns the resulting Index + */ + public build(): Index { + this.#index.validate(); + + return this.#index; + } + + /** + * Resets the builder to a previously serialized index + * @param json - result of Index.serialize() + */ + public from(json: string): this; + /** + * Resets the builder to a copy of an existing index + * @param index - source index to copy + */ + public from(index: Index): this; + /** + * Resets the builder to a copy of an existing index, or one parsed from its serialized form + * @param indexOrJSON - source index, or its serialized form + */ + public from(indexOrJSON: Index | string): this { + if (typeof indexOrJSON === 'string') { + this.#index = Index.parse(indexOrJSON); + + return this; + } + + this.#index = indexOrJSON.clone(); + + return this; + } +} diff --git a/packages/model/src/entities/Index/index.ts b/packages/model-types/src/Index/index.ts similarity index 86% rename from packages/model/src/entities/Index/index.ts rename to packages/model-types/src/Index/index.ts index 91162926..0eaae773 100644 --- a/packages/model/src/entities/Index/index.ts +++ b/packages/model-types/src/Index/index.ts @@ -1,6 +1,17 @@ -import type { DocumentIndex, TextRange } from '../../EventBus/index.js'; -import type { DataKey } from '../BlockNode/index.js'; -import type { BlockTuneName } from '../BlockTune/index.js'; +import type { BlockTuneName } from '../BlockTune.js'; +import type { DataKey } from '../DataKey.js'; +import type { DocumentId } from '../indexing.js'; +import type { TextRange } from '../Text.js'; + +/** + * Shape of the JSON root object produced for a composite serialized index (see {@link Index.serialize}) + */ +interface CompositeIndexRoot { + /** + * Serialized form of each text index segment + */ + composite: unknown; +} /** * Class representing index in the document model tree @@ -37,9 +48,9 @@ export class Index { public propertyName?: string; /** - * Document id + * Identifier of the document this index belongs to */ - public documentId?: DocumentIndex; + public documentId?: DocumentId; /** * Cross-input selection: one text index per affected input, in document order @@ -48,7 +59,7 @@ export class Index { /** * Parse serialized index - * @param serialized - serialized index + * @param serialized - JSON string produced by Index.serialize() */ public static parse(serialized: string): Index { const outer = JSON.parse(serialized) as unknown; @@ -70,7 +81,7 @@ export class Index { switch (type) { case 'doc': - index.documentId = data as DocumentIndex; + index.documentId = data as DocumentId; break; case 'prop': index.propertyName = data; @@ -114,7 +125,7 @@ export class Index { * @param outer - value returned by `JSON.parse` for a composite serialized index */ private static parseCompositeIndexFromObject(outer: object): Index { - const composite = (outer as { composite: unknown }).composite; + const composite = (outer as CompositeIndexRoot).composite; if (!Array.isArray(composite)) { throw new Error('Invalid composite index'); @@ -209,7 +220,6 @@ export class Index { || this.propertyName !== undefined || this.documentId !== undefined; - /* Stryker disable next-line ConditionalExpression -- `if (!hasOtherFields)` inverts throw vs continue; pure composite + per-field root tests already assert both sides */ if (hasOtherFields) { throw new Error('Invalid index'); } @@ -255,12 +265,10 @@ export class Index { * Returns true if index points to the text data */ public get isTextIndex(): boolean { - /* Stryker disable next-line ConditionalExpression -- inverted `if` swaps composite early return vs text predicate; composite + .isTextIndex specs cover both */ if (this.compositeSegments !== undefined && this.compositeSegments.length > 0) { return false; } - /* Stryker disable next-line ConditionalExpression, LogicalOperator -- compound text-index predicate; `&&`↔`||` covered by .isTextIndex + getTextSegments specs */ return this.blockIndex !== undefined && this.dataKey !== undefined && this.textRange !== undefined; } @@ -268,7 +276,6 @@ export class Index { * Returns true if index points to the block node */ public get isBlockIndex(): boolean { - /* Stryker disable next-line ConditionalExpression -- compound block-index predicate; .isBlockIndex specs cover field combinations */ return this.blockIndex !== undefined && this.tuneName === undefined && this.dataKey === undefined && this.textRange === undefined; } @@ -276,7 +283,6 @@ export class Index { * Returns true if index points to the block node data key */ public get isDataIndex(): boolean { - /* Stryker disable next-line ConditionalExpression, LogicalOperator -- compound data-index predicate; .isDataIndex specs cover field combinations */ return this.blockIndex !== undefined && this.tuneName === undefined && this.dataKey !== undefined && this.textRange === undefined; } } diff --git a/packages/model-types/src/InlineTool.ts b/packages/model-types/src/InlineTool.ts new file mode 100644 index 00000000..82e8515a --- /dev/null +++ b/packages/model-types/src/InlineTool.ts @@ -0,0 +1,11 @@ +import { create, type Nominal } from './Nominal.js'; + +export type InlineToolName = Nominal; + +export type InlineToolData = Nominal, 'InlineToolData'>; + +/** Function returns a value with the nominal InlineToolName type */ +export const createInlineToolName = create(); + +/** Function to cast values to InlineToolData type */ +export const createInlineToolData = create(); diff --git a/packages/model-types/src/IntersectType.ts b/packages/model-types/src/IntersectType.ts new file mode 100644 index 00000000..f5018368 --- /dev/null +++ b/packages/model-types/src/IntersectType.ts @@ -0,0 +1,9 @@ +/** Describes how overlapping formatting ranges should be resolved */ +export enum IntersectType { + /** Extend the existing range */ + Extend = 'extend', + /** Replace the existing range */ + Replace = 'replace', + /** Keep both ranges */ + LeaveBoth = 'leave-both' +} diff --git a/packages/model/src/utils/Nominal.ts b/packages/model-types/src/Nominal.ts similarity index 50% rename from packages/model/src/utils/Nominal.ts rename to packages/model-types/src/Nominal.ts index bf83a060..253bd3c1 100644 --- a/packages/model/src/utils/Nominal.ts +++ b/packages/model-types/src/Nominal.ts @@ -1,14 +1,27 @@ -/* Stryker disable next-line StringLiteral */ +/** + * Property key used to brand the nominal type's identifier + */ const nominalField = '_nominal_'; -/* Stryker disable next-line StringLiteral */ + +/** + * Property key used to brand the nominal type's base type + */ const baseTypeField = '_baseType_'; /** - * An alias for creating a nominal type + * Nominal type branding helper. + * Use to create opaque types that are structurally compatible at runtime but distinct at compile time. + * Must match the Nominal type in @editorjs/model for cross-package structural compatibility. */ export type Nominal = Type & - { readonly [nominalField]: Identifier } & - { readonly [baseTypeField]: Type }; + { + /** Brand carrying the nominal type's identifier */ + readonly [nominalField]: Identifier; + } & + { + /** Brand carrying the nominal type's base type */ + readonly [baseTypeField]: Type; + }; /** * Alias returns base type of Nominal diff --git a/packages/model-types/src/Text.ts b/packages/model-types/src/Text.ts new file mode 100644 index 00000000..6e511036 --- /dev/null +++ b/packages/model-types/src/Text.ts @@ -0,0 +1,29 @@ +import type { InlineToolData, InlineToolName } from './InlineTool.js'; +import type { BlockChildType, NODE_TYPE_HIDDEN_PROP } from './BlockChildType.js'; + +/** Range represented as [start, end] */ +export type TextRange = [number, number]; + +/** Fragment of text with inline formatting tool applied */ +export interface InlineFragment { + /** Inline tool name used for formatting */ + tool: InlineToolName; + /** Optional data passed to the inline tool */ + data?: InlineToolData; + /** Character range the formatting applies to */ + range: [start: number, end: number]; +} + +/** Serialized inline tree node containing text value and formatting fragments */ +export interface InlineTreeNodeSerialized { + /** Text content */ + value: string; + /** Formatting fragments applied to the text */ + fragments: InlineFragment[]; +} + +/** Text node data extending serialized inline tree with hidden type marker */ +export interface TextNodeSerialized extends InlineTreeNodeSerialized { + /** Hidden property marking this node as a text child */ + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text; +} diff --git a/packages/model-types/src/Value.ts b/packages/model-types/src/Value.ts new file mode 100644 index 00000000..492d0271 --- /dev/null +++ b/packages/model-types/src/Value.ts @@ -0,0 +1,10 @@ +import type { BlockChildType, NODE_TYPE_HIDDEN_PROP } from './BlockChildType.js'; + +/** Interface used to brand value data with a hidden type marker */ +export interface ValueBrand { + /** Hidden property marking this node as a value child */ + [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Value; +} + +/** Conditional type that adds value branding when V is an object-like type */ +export type ValueSerialized = V extends Record ? V & ValueBrand : V; diff --git a/packages/model/src/EventBus/events/BlockAddedEvent.ts b/packages/model-types/src/events/BlockAddedEvent.ts similarity index 61% rename from packages/model/src/EventBus/events/BlockAddedEvent.ts rename to packages/model-types/src/events/BlockAddedEvent.ts index e584a322..13649472 100644 --- a/packages/model/src/EventBus/events/BlockAddedEvent.ts +++ b/packages/model-types/src/events/BlockAddedEvent.ts @@ -1,7 +1,7 @@ -import type { BlockNodeSerialized } from '../../entities/BlockNode/types/index.js'; -import type { Index } from '../../entities/Index/index.js'; -import { EventAction } from '../types/EventAction.js'; -import { BaseDocumentEvent } from './BaseEvent.js'; +import { EventAction } from '../EventAction.js'; +import { BaseDocumentEvent } from '../BaseDocumentEvent.js'; +import type { Index } from '../Index/index.js'; +import type { BlockNodeSerialized } from '../BlockNode.js'; /** * BlockAdded Custom Event @@ -14,7 +14,6 @@ export class BlockAddedEvent extends BaseDocumentEvent { /** * CaretManagerCaretAddedEvent class constructor - * @param payload - event payload + * @param payload - serialized caret data for the newly added caret */ constructor(payload: CaretSerialized) { - // Stryker disable next-line ObjectLiteral super(EventType.CaretManagerUpdated, { detail: payload, }); diff --git a/packages/model/src/EventBus/events/CaretManagerCaretRemovedEvent.ts b/packages/model-types/src/events/CaretManagerCaretRemovedEvent.ts similarity index 60% rename from packages/model/src/EventBus/events/CaretManagerCaretRemovedEvent.ts rename to packages/model-types/src/events/CaretManagerCaretRemovedEvent.ts index e54252aa..c77bf87b 100644 --- a/packages/model/src/EventBus/events/CaretManagerCaretRemovedEvent.ts +++ b/packages/model-types/src/events/CaretManagerCaretRemovedEvent.ts @@ -1,5 +1,5 @@ -import { EventType } from '../types/EventType.js'; -import type { CaretSerialized } from '../../CaretManagement/index.js'; +import { EventType } from '../EventType.js'; +import type { CaretSerialized } from './CaretManagerCaretUpdatedEvent.js'; /** * CaretManagerCaretRemoved Custom Event @@ -7,10 +7,9 @@ import type { CaretSerialized } from '../../CaretManagement/index.js'; export class CaretManagerCaretRemovedEvent extends CustomEvent { /** * CaretManagerCaretRemovedEvent class constructor - * @param payload - event payload + * @param payload - serialized caret data for the removed caret */ constructor(payload: CaretSerialized) { - // Stryker disable next-line ObjectLiteral super(EventType.CaretManagerUpdated, { detail: payload, }); diff --git a/packages/model-types/src/events/CaretManagerCaretUpdatedEvent.ts b/packages/model-types/src/events/CaretManagerCaretUpdatedEvent.ts new file mode 100644 index 00000000..c0fd627b --- /dev/null +++ b/packages/model-types/src/events/CaretManagerCaretUpdatedEvent.ts @@ -0,0 +1,31 @@ +import { EventType } from '../EventType.js'; + +/** + * Payload broadcast when a caret moves or is removed + */ +export interface CaretSerialized { + /** + * Identifier of the user the caret belongs to + */ + readonly userId: string | number; + + /** + * Serialized caret index, or null if the caret was removed + */ + readonly index: string | null; +} + +/** + * CaretManagerCaretUpdated Custom Event + */ +export class CaretManagerCaretUpdatedEvent extends CustomEvent { + /** + * CaretManagerCaretUpdatedEvent class constructor + * @param payload - serialized caret data + */ + constructor(payload: CaretSerialized) { + super(EventType.CaretManagerUpdated, { + detail: payload, + }); + } +} diff --git a/packages/model/src/EventBus/events/DataNodeAddedEvent.ts b/packages/model-types/src/events/DataNodeAddedEvent.ts similarity index 61% rename from packages/model/src/EventBus/events/DataNodeAddedEvent.ts rename to packages/model-types/src/events/DataNodeAddedEvent.ts index daa162ed..284bfa5c 100644 --- a/packages/model/src/EventBus/events/DataNodeAddedEvent.ts +++ b/packages/model-types/src/events/DataNodeAddedEvent.ts @@ -1,7 +1,7 @@ -import type { BlockNodeDataSerializedValue } from '../../entities/BlockNode/types/index.js'; -import type { Index } from '../../entities/Index/index.js'; -import { EventAction } from '../types/EventAction.js'; -import { BaseDocumentEvent } from './BaseEvent.js'; +import type { BlockNodeDataSerializedValue } from '../BlockNode.js'; +import type { Index } from '../Index/index.js'; +import { EventAction } from '../EventAction.js'; +import { BaseDocumentEvent } from '../BaseDocumentEvent.js'; /** * DataNodeAdded Custom Event @@ -14,7 +14,6 @@ export class DataNodeAddedEvent extends BaseDocumentEvent /** * TextAddedEvent class constructor * @param index - index of the added text in the document - * @param text - added text - * @param userId - user identifier + * @param text - text content that was inserted + * @param userId - identifier of the user making the change */ constructor(index: Index, text: string, userId: string | number) { super(index, EventAction.Added, text, userId); diff --git a/packages/model-types/src/events/TextFormattedEvent.ts b/packages/model-types/src/events/TextFormattedEvent.ts new file mode 100644 index 00000000..0af86d8d --- /dev/null +++ b/packages/model-types/src/events/TextFormattedEvent.ts @@ -0,0 +1,34 @@ +import { EventAction } from '../EventAction.js'; +import { BaseDocumentEvent } from '../BaseDocumentEvent.js'; +import type { Index } from '../Index/index.js'; +import type { InlineToolData, InlineToolName } from '../InlineTool.js'; + +/** + * Identifies which inline tool was applied and with what options + */ +export interface TextFormattedEventData { + /** + * Name of the applied inline tool + */ + tool: InlineToolName; + + /** + * Optional data passed to the inline tool + */ + data?: InlineToolData; +} + +/** + * TextFormatted Custom Event + */ +export class TextFormattedEvent extends BaseDocumentEvent { + /** + * TextFormattedEvent class constructor + * @param index - index of the formatted text in the document + * @param data - formatting tool and its data + * @param userId - identifier of the user making the change + */ + constructor(index: Index, data: TextFormattedEventData, userId: string | number) { + super(index, EventAction.Modified, data, userId); + } +} diff --git a/packages/model/src/EventBus/events/TextRemovedEvent.ts b/packages/model-types/src/events/TextRemovedEvent.ts similarity index 57% rename from packages/model/src/EventBus/events/TextRemovedEvent.ts rename to packages/model-types/src/events/TextRemovedEvent.ts index ca0dfe7e..1126720e 100644 --- a/packages/model/src/EventBus/events/TextRemovedEvent.ts +++ b/packages/model-types/src/events/TextRemovedEvent.ts @@ -1,6 +1,6 @@ -import type { Index } from '../../entities/Index/index.js'; -import { EventAction } from '../types/EventAction.js'; -import { BaseDocumentEvent } from './BaseEvent.js'; +import { EventAction } from '../EventAction.js'; +import { BaseDocumentEvent } from '../BaseDocumentEvent.js'; +import type { Index } from '../Index/index.js'; /** * TextRemoved Custom Event @@ -9,8 +9,8 @@ export class TextRemovedEvent extends BaseDocumentEvent { + /** + * TextUnformattedEvent class constructor + * @param index - index of the unformatted text in the document + * @param data - formatting tool and its data + * @param userId - identifier of the user making the change + */ + constructor(index: Index, data: TextUnformattedEventData, userId: string | number) { + super(index, EventAction.Modified, data, userId); + } +} diff --git a/packages/model/src/EventBus/events/TuneModifiedEvent.ts b/packages/model-types/src/events/TuneModifiedEvent.ts similarity index 69% rename from packages/model/src/EventBus/events/TuneModifiedEvent.ts rename to packages/model-types/src/events/TuneModifiedEvent.ts index 6eff85b1..7af0e2fd 100644 --- a/packages/model/src/EventBus/events/TuneModifiedEvent.ts +++ b/packages/model-types/src/events/TuneModifiedEvent.ts @@ -1,7 +1,7 @@ -import type { Index } from '../../entities/Index/index.js'; -import type { ModifiedEventData } from './BaseEvent.js'; -import { EventAction } from '../types/EventAction.js'; -import { BaseDocumentEvent } from './BaseEvent.js'; +import { EventAction } from '../EventAction.js'; +import { BaseDocumentEvent } from '../BaseDocumentEvent.js'; +import type { Index } from '../Index/index.js'; +import type { ModifiedEventData } from '../EventBus.js'; /** * TuneModified Custom Event diff --git a/packages/model/src/EventBus/events/ValueModifiedEvent.ts b/packages/model-types/src/events/ValueModifiedEvent.ts similarity index 69% rename from packages/model/src/EventBus/events/ValueModifiedEvent.ts rename to packages/model-types/src/events/ValueModifiedEvent.ts index a0cf8027..cd981715 100644 --- a/packages/model/src/EventBus/events/ValueModifiedEvent.ts +++ b/packages/model-types/src/events/ValueModifiedEvent.ts @@ -1,7 +1,7 @@ -import type { Index } from '../../entities/Index/index.js'; -import type { ModifiedEventData } from './BaseEvent.js'; -import { EventAction } from '../types/EventAction.js'; -import { BaseDocumentEvent } from './BaseEvent.js'; +import type { Index } from '../Index/index.js'; +import type { ModifiedEventData } from '../BaseDocumentEvent.js'; +import { EventAction } from '../EventAction.js'; +import { BaseDocumentEvent } from '../BaseDocumentEvent.js'; /** * ValueModified Custom Event diff --git a/packages/model-types/src/events/index.ts b/packages/model-types/src/events/index.ts new file mode 100644 index 00000000..a234ce00 --- /dev/null +++ b/packages/model-types/src/events/index.ts @@ -0,0 +1,14 @@ +export { DataNodeAddedEvent } from './DataNodeAddedEvent.js'; +export { DataNodeRemovedEvent } from './DataNodeRemovedEvent.js'; +export { ValueModifiedEvent } from './ValueModifiedEvent.js'; +export { TextAddedEvent } from './TextAddedEvent.js'; +export { TextRemovedEvent } from './TextRemovedEvent.js'; +export { TextFormattedEvent, type TextFormattedEventData } from './TextFormattedEvent.js'; +export { TextUnformattedEvent, type TextUnformattedEventData } from './TextUnformattedEvent.js'; +export { CaretManagerCaretUpdatedEvent, type CaretSerialized } from './CaretManagerCaretUpdatedEvent.js'; +export { CaretManagerCaretAddedEvent } from './CaretManagerCaretAddedEvent.js'; +export { CaretManagerCaretRemovedEvent } from './CaretManagerCaretRemovedEvent.js'; +export { BlockAddedEvent } from './BlockAddedEvent.js'; +export { BlockRemovedEvent } from './BlockRemovedEvent.js'; +export { PropertyModifiedEvent } from './PropertyModifiedEvent.js'; +export { TuneModifiedEvent } from './TuneModifiedEvent.js'; diff --git a/packages/model-types/src/index.ts b/packages/model-types/src/index.ts new file mode 100644 index 00000000..2d428ad4 --- /dev/null +++ b/packages/model-types/src/index.ts @@ -0,0 +1,106 @@ +export type { Nominal } from './Nominal.js'; +export { create } from './Nominal.js'; +export type { + BlockId, + BlockIndexOrId, + BlockToolName +} from './BlockId.js'; +export { + createBlockId, + generateBlockId, + createBlockToolName +} from './BlockId.js'; +export type { + BlockTuneName, + BlockTuneSerialized +} from './BlockTune.js'; +export { createBlockTuneName } from './BlockTune.js'; +export type { DataKey } from './DataKey.js'; +export { createDataKey } from './DataKey.js'; +export type { + BlockChildNodeSerialized, + BlockNodeDataSerializedValue, + BlockNodeDataSerialized, + BlockData, + BlockNodeSerialized, + BlockNodeInit +} from './BlockNode.js'; +export type { + TextRange, + InlineFragment, + InlineTreeNodeSerialized, + TextNodeSerialized +} from './Text.js'; +export type { + ValueSerialized, + ValueBrand +} from './Value.js'; +export type { + InlineToolName, + InlineToolData +} from './InlineTool.js'; +export { + createInlineToolName, + createInlineToolData +} from './InlineTool.js'; +export type { + Properties, + DocumentData, + EditorDocumentSerialized +} from './EditorDocument.js'; +export type { + ChangeData +} from './ChangeData.js'; +export type { Caret } from './Caret.js'; + +// TODO: keypath is a generic object-traversal utility unrelated to the model +// domain; it lives here only so both `model` and `sdk` (which must not depend +// on each other) can share one implementation. Extract to a standalone +// `@editorjs/utils` package and depend on that from here, `model`, and `sdk`. +export { get, set, has, insert, remove, renumberKeys } from './keypath.js'; +export { + EventBus, + type ModifiedEventData +} from './EventBus.js'; +export type { + BlockEvents, + TextNodeEvents, + ValueNodeEvents, + BlockTuneEvents, + DocumentEvents, + ModelEvents, + CaretManagerEvents +} from './EventMap.js'; + +export { NODE_TYPE_HIDDEN_PROP, BlockChildType } from './BlockChildType.js'; +export { FormattingAction } from './FormattingAction.js'; +export { IntersectType } from './IntersectType.js'; + +export { EventAction } from './EventAction.js'; +export { EventType } from './EventType.js'; +export type { DocumentId, DocumentIndex } from './indexing.js'; +export { Index } from './Index/index.js'; +export { IndexBuilder } from './Index/IndexBuilder.js'; +export { + BaseDocumentEvent, + type EventPayloadBase +} from './BaseDocumentEvent.js'; +export { + DataNodeAddedEvent, + DataNodeRemovedEvent, + ValueModifiedEvent, + TextAddedEvent, + TextRemovedEvent, + TextFormattedEvent, + TextUnformattedEvent, + type TextFormattedEventData, + type TextUnformattedEventData, + CaretManagerCaretUpdatedEvent, + type CaretSerialized, + CaretManagerCaretAddedEvent, + CaretManagerCaretRemovedEvent, + BlockAddedEvent, + BlockRemovedEvent, + PropertyModifiedEvent, + TuneModifiedEvent +} from './events/index.js'; diff --git a/packages/model-types/src/indexing.ts b/packages/model-types/src/indexing.ts new file mode 100644 index 00000000..ea2ee874 --- /dev/null +++ b/packages/model-types/src/indexing.ts @@ -0,0 +1,11 @@ +import type { Nominal } from './Nominal.js'; + +/** + * Nominal type for document identifiers + */ +export type DocumentId = Nominal; + +/** + * Alias for DocumentId used in event indexing + */ +export type DocumentIndex = DocumentId; diff --git a/packages/model/src/utils/keypath.spec.ts b/packages/model-types/src/keypath.spec.ts similarity index 95% rename from packages/model/src/utils/keypath.spec.ts rename to packages/model-types/src/keypath.spec.ts index 047dd4af..088211ab 100644 --- a/packages/model/src/utils/keypath.spec.ts +++ b/packages/model-types/src/keypath.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { get, has, insert, remove, set, renumberKeys } from './keypath.js'; +import { get, has, insert, remove, set, renumberKeys } from './keypath'; describe('keypath util', () => { const value = 'value'; @@ -57,7 +57,7 @@ describe('keypath util', () => { insert(object, 'a.b.1', 'y'); - expect(object.a.b).toEqual(['x', 'y', 'z']); + expect((object.a as Record<'b', string[]>).b).toEqual(['x', 'y', 'z']); }); it('should accept keys as an array', () => { @@ -83,7 +83,7 @@ describe('keypath util', () => { set(object, 'a.b.c', value); - expect(object.a.b.c).toEqual(value); + expect((object.a as Record<'b', Record<'c', string>>).b.c).toEqual(value); }); it('should created nested objects by string key parts when keys passed as an array', () => { @@ -91,7 +91,7 @@ describe('keypath util', () => { set(object, ['a', 'b', 'c'], value); - expect(object.a.b.c).toEqual(value); + expect((object.a as Record>).b.c).toEqual(value); }); it('should update existing value', () => { @@ -144,7 +144,7 @@ describe('keypath util', () => { set(object, 'a.0', value); - expect(object.a[0]).toEqual(value); + expect((object.a as unknown[])[0]).toEqual(value); }); it('should insert value into an array by the correct index for numeric key parts when index is greater than array length', () => { @@ -160,7 +160,7 @@ describe('keypath util', () => { set(object, 'a.0.b', value); - expect(object.a[0].b).toEqual(value); + expect((object.a as Array>)[0].b).toEqual(value); }); }); @@ -338,7 +338,7 @@ describe('keypath util', () => { remove(object, 'a.b.c'); - expect(object.a.b).not.toHaveProperty('c'); + expect((object.a as Record>).b).not.toHaveProperty('c'); }); it('should not affect sibling properties when removing a nested property', () => { diff --git a/packages/model/src/utils/keypath.ts b/packages/model-types/src/keypath.ts similarity index 80% rename from packages/model/src/utils/keypath.ts rename to packages/model-types/src/keypath.ts index 1c90e212..69de37fc 100644 --- a/packages/model/src/utils/keypath.ts +++ b/packages/model-types/src/keypath.ts @@ -3,8 +3,8 @@ * @param data - object to get value from * @param keys - keypath to a value */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- unknown can't be used as data parameter is used for recursion -export function get(data: Record, keys: string | string[]): T | undefined { +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed for recursive keypath traversal +function get(data: Record, keys: string | string[]): T | undefined { const parsedKeys = Array.isArray(keys) ? keys : keys.split('.'); const key = parsedKeys.shift(); @@ -23,10 +23,10 @@ export function get(data: Record, ke * Set value to object by keypath * @param data - object to set value to * @param keys - keypath to a value - * @param value - value to set + * @param value - new value to assign at the keypath */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- unknown can't be used as data parameter is used for recursion -export function set(data: Record, keys: string | string[], value: T): void { +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed for recursive keypath traversal +function set(data: Record, keys: string | string[], value: T): void { const parsedKeys = Array.isArray(keys) ? keys : keys.split('.'); const key = parsedKeys.shift(); @@ -52,8 +52,8 @@ export function set(data: Record, ke * @param data - object to check * @param keys - keypath to a value */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- unknown can't be used as data parameter is used for recursion -export function has(data: Record, keys: string | string[]): boolean { +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed for recursive keypath traversal +function has(data: Record, keys: string | string[]): boolean { return get(data, keys) !== undefined; } @@ -64,8 +64,8 @@ export function has(data: Record, keys: string | * @param keys - keypath where the last segment is the target index * @param value - value to splice in */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- unknown can't be used as data parameter is used for recursion -export function insert(data: Record, keys: string | string[], value: T): void { +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed for recursive keypath traversal +function insert(data: Record, keys: string | string[], value: T): void { const parsedKeys = Array.isArray(keys) ? [...keys] : keys.split('.'); if (parsedKeys.length === 0) { @@ -73,7 +73,7 @@ export function insert(data: Record, } const lastKey = parsedKeys.pop()!; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- unknown can't be used as data parameter is used for recursion + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed for generic array access const parent = get(data, parsedKeys); if (!Array.isArray(parent)) { @@ -97,8 +97,8 @@ export function insert(data: Record, * @param data - object to remove value from * @param keys - keypath to the value to remove */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- unknown can't be used as data parameter is used for recursion -export function remove(data: Record, keys: string | string[]): void { +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed for recursive keypath traversal +function remove(data: Record, keys: string | string[]): void { const parsedKeys = Array.isArray(keys) ? [...keys] : keys.split('.'); if (parsedKeys.length === 0) { @@ -106,7 +106,7 @@ export function remove(data: Record, keys: string } const lastKey = parsedKeys.pop()!; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- unknown can't be used as data parameter is used for recursion + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed for generic object access const parent = get>(data, parsedKeys); if (parent === undefined || parent === null) { @@ -137,7 +137,7 @@ export function remove(data: Record, keys: string * @param keys - flat dot-notation data keys to renumber * @returns a map of original key → renumbered key */ -export function renumberKeys(keys: string[]): Map { +function renumberKeys(keys: string[]): Map { const minArrayIndices = new Map(); for (const key of keys) { @@ -180,3 +180,5 @@ export function renumberKeys(keys: string[]): Map { return result; } + +export { get, set, has, insert, remove, renumberKeys }; diff --git a/packages/model-types/tsconfig.build.json b/packages/model-types/tsconfig.build.json new file mode 100644 index 00000000..832aea87 --- /dev/null +++ b/packages/model-types/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/packages/model-types/tsconfig.eslint.json b/packages/model-types/tsconfig.eslint.json new file mode 100644 index 00000000..fe43d73e --- /dev/null +++ b/packages/model-types/tsconfig.eslint.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "types": ["node", "jest"], + }, + "extends": "./tsconfig.json", + "include": [ + "src/**/*", + "eslint.config.mjs", + "**/*.spec.ts" + ], + "exclude": [ + "dist/**/*", + "node_modules/**/*", + ] +} diff --git a/packages/model-types/tsconfig.json b/packages/model-types/tsconfig.json new file mode 100644 index 00000000..0be6a7d3 --- /dev/null +++ b/packages/model-types/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "Node", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "composite": true, + "rootDir": "src/" + }, + "include": ["src/**/*"], + "exclude": [ + "node_modules/**/*", + "dist/**/*" + ] +} diff --git a/packages/model/jest.config.ts b/packages/model/jest.config.ts index e8a9f307..dd23e6ae 100644 --- a/packages/model/jest.config.ts +++ b/packages/model/jest.config.ts @@ -11,13 +11,22 @@ export default { }, coverageReporters: ['lcov', 'json-summary', 'text-summary'], transform: { - // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` - // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` '^.+\\.tsx?$': [ 'ts-jest', { useESM: true, }, ], + '^.+\\.jsx?$': [ + 'babel-jest', + { + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + ], + }, + ], }, + transformIgnorePatterns: [ + 'node_modules/(?!@editorjs)', + ], } as JestConfigWithTsJest; diff --git a/packages/model/package.json b/packages/model/package.json index 4b68365e..6d8eb571 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -17,17 +17,22 @@ "test:mutations": "stryker run", "clear": "rm -rf ./dist && rm -rf ./tsconfig.build.tsbuildinfo" }, + "dependencies": { + "@editorjs/model-types": "workspace:^" + }, "devDependencies": { + "@babel/preset-env": "^8.0.2", "@jest/globals": "^29.7.0", "@stryker-mutator/core": "^7.0.2", "@stryker-mutator/jest-runner": "^7.0.2", "@stryker-mutator/typescript-checker": "^7.0.2", "@types/eslint": "^8", - "@types/jest": "^29.5.1", + "@types/jest": "^30.0.0", + "babel-jest": "^30.4.1", "eslint": "^8.38.0", "eslint-config-codex": "^2.0.3", "eslint-plugin-import": "^2.29.0", - "jest": "^29.7.0", + "jest": "^30.4.2", "stryker-cli": "^1.0.2", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", diff --git a/packages/model/src/CaretManagement/Caret/Caret.spec.ts b/packages/model/src/CaretManagement/Caret/Caret.spec.ts index 378ee2eb..ae160d5f 100644 --- a/packages/model/src/CaretManagement/Caret/Caret.spec.ts +++ b/packages/model/src/CaretManagement/Caret/Caret.spec.ts @@ -1,4 +1,4 @@ -import { Index } from '../../entities/index.js'; +import { Index } from '@editorjs/model-types'; import { Caret } from './Caret.js'; import { CaretEvent, CaretUpdatedEvent } from './types.js'; import { jest } from '@jest/globals'; diff --git a/packages/model/src/CaretManagement/Caret/Caret.ts b/packages/model/src/CaretManagement/Caret/Caret.ts index f21ec968..27e601cb 100644 --- a/packages/model/src/CaretManagement/Caret/Caret.ts +++ b/packages/model/src/CaretManagement/Caret/Caret.ts @@ -1,25 +1,6 @@ -import type { Index } from '../../entities/Index/index.js'; -import type { CaretSerialized, CaretEvent } from './types.js'; +import type { Index, CaretSerialized } from '@editorjs/model-types'; import { CaretUpdatedEvent } from './types.js'; -import { EventBus } from '../../EventBus/index.js'; - -/** - * Interface to extend EventTarget methods - */ -export interface Caret { - /** - * Adds CaretEvent listener - * @param event - type of event - * @param listener - listener - */ - addEventListener(event: CaretEvent, listener: (event: K) => void): void; - - /** - * Dispatches CaretUpdatedEvent - * @param event - event to dispatch - */ - dispatchEvent(event: CaretUpdatedEvent): boolean; -} +import { EventBus } from '@editorjs/model-types'; /** * Caret is responsible for storing caret index diff --git a/packages/model/src/CaretManagement/Caret/types.ts b/packages/model/src/CaretManagement/Caret/types.ts index d25b027c..3ee6c51b 100644 --- a/packages/model/src/CaretManagement/Caret/types.ts +++ b/packages/model/src/CaretManagement/Caret/types.ts @@ -1,20 +1,6 @@ +import type { CaretSerialized } from '@editorjs/model-types'; import type { Caret } from './Caret.js'; -/** - * Caret serialized data - */ -export interface CaretSerialized { - /** - * Caret id - */ - readonly userId: string | number; - - /** - * Caret index - */ - readonly index: string | null; -} - /** * Enumeration of Caret events */ diff --git a/packages/model/src/CaretManagement/CaretManager.spec.ts b/packages/model/src/CaretManagement/CaretManager.spec.ts index f4376a3a..82881d6f 100644 --- a/packages/model/src/CaretManagement/CaretManager.spec.ts +++ b/packages/model/src/CaretManagement/CaretManager.spec.ts @@ -1,10 +1,4 @@ -import { Index } from '../entities/index.js'; -import { - CaretManagerCaretAddedEvent, - CaretManagerCaretRemovedEvent, - CaretManagerCaretUpdatedEvent, - EventType -} from '../EventBus/index.js'; +import { Index, CaretManagerCaretAddedEvent, CaretManagerCaretRemovedEvent, CaretManagerCaretUpdatedEvent, EventType } from '@editorjs/model-types'; import { Caret } from './Caret/index.js'; import { CaretManager } from './CaretManager.js'; import { jest } from '@jest/globals'; diff --git a/packages/model/src/CaretManagement/CaretManager.ts b/packages/model/src/CaretManagement/CaretManager.ts index 245acc4d..d37f5c41 100644 --- a/packages/model/src/CaretManagement/CaretManager.ts +++ b/packages/model/src/CaretManagement/CaretManager.ts @@ -1,10 +1,10 @@ -import type { Index } from '../entities/Index/index.js'; +import type { Index } from '@editorjs/model-types'; import { CaretManagerCaretAddedEvent, CaretManagerCaretRemovedEvent, CaretManagerCaretUpdatedEvent, EventBus -} from '../EventBus/index.js'; +} from '@editorjs/model-types'; import { Caret } from './Caret/Caret.js'; import { CaretEvent } from './Caret/types.js'; @@ -36,7 +36,7 @@ export class CaretManager extends EventBus { this.#registry.set(caret.userId, caret); - caret.addEventListener(CaretEvent.Updated, event => this.updateCaret(event.detail)); + caret.addEventListener(CaretEvent.Updated, (event: Event) => this.updateCaret((event as CustomEvent).detail)); this.dispatchEvent(new CaretManagerCaretAddedEvent(caret.toJSON())); diff --git a/packages/model/src/CaretManagement/index.ts b/packages/model/src/CaretManagement/index.ts index 13ee2dbc..98cadc34 100644 --- a/packages/model/src/CaretManagement/index.ts +++ b/packages/model/src/CaretManagement/index.ts @@ -1,3 +1,2 @@ export * from './Caret/Caret.js'; export * from './CaretManager.js'; -export type { CaretSerialized } from './Caret/types.js'; diff --git a/packages/model/src/EditorJSModel.integration.spec.ts b/packages/model/src/EditorJSModel.integration.spec.ts index 35369038..1fc6b8c3 100644 --- a/packages/model/src/EditorJSModel.integration.spec.ts +++ b/packages/model/src/EditorJSModel.integration.spec.ts @@ -1,5 +1,5 @@ -import { EventType } from './EventBus/types/EventType.js'; -import { BlockAddedEvent } from './EventBus/events/index.js'; +import { EventType } from '@editorjs/model-types'; +import { BlockAddedEvent } from '@editorjs/model-types'; import { EditorJSModel } from './EditorJSModel.js'; import { data } from './mocks/data.js'; diff --git a/packages/model/src/EditorJSModel.spec.ts b/packages/model/src/EditorJSModel.spec.ts index 28d29bc4..c6f37472 100644 --- a/packages/model/src/EditorJSModel.spec.ts +++ b/packages/model/src/EditorJSModel.spec.ts @@ -1,9 +1,8 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ import { beforeEach, describe } from '@jest/globals'; import { EditorJSModel } from './EditorJSModel.js'; -import { createDataKey, IndexBuilder } from './entities/index.js'; -import type { DocumentId } from './EventBus/index.js'; -import type { BlockId } from './entities/BlockNode/index.js'; +import { createDataKey, IndexBuilder, type BlockId } from '@editorjs/model-types'; +import type { DocumentId } from '@editorjs/model-types'; describe('EditorJSModel', () => { it('should expose only the public API', () => { diff --git a/packages/model/src/EditorJSModel.ts b/packages/model/src/EditorJSModel.ts index b1ae83c8..0f000097 100644 --- a/packages/model/src/EditorJSModel.ts +++ b/packages/model/src/EditorJSModel.ts @@ -1,17 +1,27 @@ // Stryker disable all -- we don't count mutation test coverage fot this file as it just proxy calls to EditorDocument /* istanbul ignore file -- we don't count test coverage fot this file as it just proxy calls to EditorDocument */ -import { type EditorDocumentSerialized, type Index, IndexBuilder } from './entities/index.js'; -import { type BlockNodeSerialized, type BlockNodeInit, type BlockId, type BlockIndexOrId, EditorDocument } from './entities/index.js'; +import { type BlockNodeSerialized, type BlockNodeInit, EditorDocument } from './entities/index.js'; import { BlockAddedEvent, - BlockRemovedEvent, EventAction, - EventBus, - EventType, + BlockRemovedEvent, TextAddedEvent, TextRemovedEvent -} from './EventBus/index.js'; -import type { ModelEvents, CaretManagerCaretUpdatedEvent, CaretManagerEvents } from './EventBus/index.js'; -import { BaseDocumentEvent, type ModifiedEventData } from './EventBus/events/BaseEvent.js'; +} from '@editorjs/model-types'; +import { + type EditorDocumentSerialized, + type Index, + IndexBuilder, + type BlockId, + type BlockIndexOrId, + EventAction, + EventBus, + EventType, + type ModelEvents, + type CaretManagerCaretUpdatedEvent, + type CaretManagerEvents, + BaseDocumentEvent, + type ModifiedEventData +} from '@editorjs/model-types'; import { getContext, WithContext } from './utils/Context.js'; import type { Constructor } from './utils/types.js'; import { CaretManager } from './CaretManagement/index.js'; diff --git a/packages/model/src/EventBus/EventBus.ts b/packages/model/src/EventBus/EventBus.ts deleted file mode 100644 index a6ff7b7a..00000000 --- a/packages/model/src/EventBus/EventBus.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Provides an event bus for using in the document model - * @todo Move event bus to entities folder - */ -export class EventBus extends EventTarget {} diff --git a/packages/model/src/EventBus/events/CaretManagerCaretUpdatedEvent.ts b/packages/model/src/EventBus/events/CaretManagerCaretUpdatedEvent.ts deleted file mode 100644 index 12bad917..00000000 --- a/packages/model/src/EventBus/events/CaretManagerCaretUpdatedEvent.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { EventType } from '../types/EventType.js'; -import type { CaretSerialized } from '../../CaretManagement/index.js'; - -/** - * CaretManagerCaretUpdated Custom Event - */ -export class CaretManagerCaretUpdatedEvent extends CustomEvent { - /** - * CaretManagerCaretUpdatedEvent class constructor - * @param payload - event payload - */ - constructor(payload: CaretSerialized) { - // Stryker disable next-line ObjectLiteral - super(EventType.CaretManagerUpdated, { - detail: payload, - }); - } -} diff --git a/packages/model/src/EventBus/events/TextFormattedEvent.ts b/packages/model/src/EventBus/events/TextFormattedEvent.ts deleted file mode 100644 index 4c5bd12c..00000000 --- a/packages/model/src/EventBus/events/TextFormattedEvent.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Index } from '../../entities/Index/index.js'; -import { EventAction } from '../types/EventAction.js'; -import type { InlineToolData, InlineToolName } from '../../entities/index.js'; -import { BaseDocumentEvent } from './BaseEvent.js'; - -export interface TextFormattedEventData { - tool: InlineToolName; - data?: InlineToolData; -} - -/** - * TextFormatted Custom Event - */ -export class TextFormattedEvent extends BaseDocumentEvent { - /** - * TextFormattedEvent class constructor - * @param index - index of formatted fragment in the document - * @param data - data of the InlineTool that was used to format the fragment. Optional - * @param userId - user identifier - */ - constructor(index: Index, data: TextFormattedEventData, userId: string | number) { - super(index, EventAction.Modified, data, userId); - } -} diff --git a/packages/model/src/EventBus/events/TextUnformattedEvent.ts b/packages/model/src/EventBus/events/TextUnformattedEvent.ts deleted file mode 100644 index 145485c1..00000000 --- a/packages/model/src/EventBus/events/TextUnformattedEvent.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Index } from '../../entities/Index/index.js'; -import { EventAction } from '../types/EventAction.js'; -import type { InlineToolData, InlineToolName } from '../../entities/index.js'; -import { BaseDocumentEvent } from './BaseEvent.js'; - -export interface TextUnformattedEventData { - tool: InlineToolName; - data?: InlineToolData; -} - -/** - * TextFormatted Custom Event - */ -export class TextUnformattedEvent extends BaseDocumentEvent { - /** - * TextFormattedEvent class constructor - * @param index - index of formatted fragment in the document - * @param data - data of the InlineTool that was used to format the fragment. Optional - * @param userId - user identifier - */ - constructor(index: Index, data: TextUnformattedEventData, userId: string | number) { - super(index, EventAction.Modified, data, userId); - } -} diff --git a/packages/model/src/EventBus/events/index.ts b/packages/model/src/EventBus/events/index.ts deleted file mode 100644 index 23940e25..00000000 --- a/packages/model/src/EventBus/events/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -export * from './BlockAddedEvent.js'; -export * from './BlockRemovedEvent.js'; -export * from './PropertyModifiedEvent.js'; -export * from './TextFormattedEvent.js'; -export * from './TextUnformattedEvent.js'; -export * from './TextAddedEvent.js'; -export * from './TextRemovedEvent.js'; -export * from './TuneModifiedEvent.js'; -export * from './ValueModifiedEvent.js'; -export * from './CaretManagerCaretUpdatedEvent.js'; -export * from './CaretManagerCaretAddedEvent.js'; -export * from './CaretManagerCaretRemovedEvent.js'; -export * from './DataNodeAddedEvent.js'; -export * from './DataNodeRemovedEvent.js'; -export * from './BaseEvent.js'; diff --git a/packages/model/src/EventBus/index.ts b/packages/model/src/EventBus/index.ts index bbf8cb26..6b67b077 100644 --- a/packages/model/src/EventBus/index.ts +++ b/packages/model/src/EventBus/index.ts @@ -1,7 +1,4 @@ -export * from './EventBus.js'; -export * from './events/index.js'; -export * from './types/EventAction.js'; +export { EventAction, EventType } from '@editorjs/model-types'; export type * from './types/EventMap.js'; export type * from './types/EventTarget.d.ts'; -export * from './types/EventType.js'; export type * from './types/indexing.js'; diff --git a/packages/model/src/EventBus/types/EventMap.ts b/packages/model/src/EventBus/types/EventMap.ts index e9d726a0..2bb94335 100644 --- a/packages/model/src/EventBus/types/EventMap.ts +++ b/packages/model/src/EventBus/types/EventMap.ts @@ -1,36 +1,8 @@ -import type { EventType } from './EventType.js'; +import type { EventType } from '@editorjs/model-types'; import type { - BlockAddedEvent, - BlockRemovedEvent, - TextAddedEvent, - TextRemovedEvent, - TextUnformattedEvent, - TextFormattedEvent, - ValueModifiedEvent, - TuneModifiedEvent, - PropertyModifiedEvent, - CaretManagerCaretUpdatedEvent -} from '../events/index.js'; -import type { CaretManagerCaretAddedEvent, CaretManagerCaretRemovedEvent } from '../events/index.js'; - -export { CaretManagerCaretUpdatedEvent }; - -/** - * Alias for all block events - */ -export type BlockEvents = BlockAddedEvent | BlockRemovedEvent; - -export type TextNodeEvents = TextAddedEvent | TextRemovedEvent | TextFormattedEvent | TextUnformattedEvent; - -export type ValueNodeEvents = ValueModifiedEvent; - -export type BlockTuneEvents = TuneModifiedEvent; - -export type DocumentEvents = PropertyModifiedEvent; - -export type ModelEvents = BlockEvents | TextNodeEvents | ValueNodeEvents | BlockTuneEvents | DocumentEvents; - -export type CaretManagerEvents = CaretManagerCaretAddedEvent | CaretManagerCaretUpdatedEvent | CaretManagerCaretRemovedEvent; + ModelEvents, + CaretManagerEvents +} from '@editorjs/model-types'; /** * Map of all events that can be emitted inside the DocumentModel diff --git a/packages/model/src/EventBus/types/indexing.ts b/packages/model/src/EventBus/types/indexing.ts index 523b84a8..53961f7e 100644 --- a/packages/model/src/EventBus/types/indexing.ts +++ b/packages/model/src/EventBus/types/indexing.ts @@ -1,26 +1,11 @@ -import type { Nominal } from '../../utils/Nominal.js'; - -/** - * Alias for a document id - */ -export type DocumentId = Nominal; +export type { DocumentId, DocumentIndex } from '@editorjs/model-types'; /** * Numeric id for a block node */ type BlockIndexAlias = number; -/** - * Index for a document node - */ -export type DocumentIndex = DocumentId; - /** * Index for a block node */ export type BlockIndex = [BlockIndexAlias]; - -/** - * Index for a text range - */ -export type TextRange = [number, number]; diff --git a/packages/model/src/entities/BlockNode/BlockNode.integration.spec.ts b/packages/model/src/entities/BlockNode/BlockNode.integration.spec.ts index 871f84d6..8d4b99cf 100644 --- a/packages/model/src/entities/BlockNode/BlockNode.integration.spec.ts +++ b/packages/model/src/entities/BlockNode/BlockNode.integration.spec.ts @@ -1,8 +1,7 @@ +import { BlockChildType, createInlineToolName } from '@editorjs/model-types'; +import type { InlineFragment } from '@editorjs/model-types'; import { BlockNode, createDataKey } from './index.js'; -import { BlockChildType } from './types/index.js'; -import { NODE_TYPE_HIDDEN_PROP } from './consts.js'; -import type { InlineFragment } from '../inline-fragments/index.js'; -import { createInlineToolName } from '../inline-fragments/index.js'; +import { NODE_TYPE_HIDDEN_PROP } from '@editorjs/model-types'; import { ValueNode } from '../ValueNode/index.js'; describe('BlockNode integration tests', () => { diff --git a/packages/model/src/entities/BlockNode/BlockNode.spec.ts b/packages/model/src/entities/BlockNode/BlockNode.spec.ts index 0de84617..2ca5cdae 100644 --- a/packages/model/src/entities/BlockNode/BlockNode.spec.ts +++ b/packages/model/src/entities/BlockNode/BlockNode.spec.ts @@ -1,6 +1,6 @@ -import { EventAction } from '../../EventBus/index.js'; -import { Index } from '../Index/index.js'; -import { IndexBuilder } from '../Index/IndexBuilder.js'; +import { EventAction } from '@editorjs/model-types'; +import { Index } from '@editorjs/model-types'; +import { IndexBuilder } from '@editorjs/model-types'; import { BlockNode, createBlockToolName, createDataKey, createBlockId } from './index.js'; import { NonExistingKeyError } from './errors/NonExistingKeyError.js'; @@ -10,15 +10,16 @@ import { ValueNode } from '../ValueNode/index.js'; import type { EditorDocument } from '../EditorDocument/index.js'; import type { ValueNodeConstructorParameters } from '../ValueNode/index.js'; -import type { InlineFragment, InlineToolData, InlineToolName, TextNodeSerialized } from '../inline-fragments/index.js'; +import type { InlineToolData, InlineToolName } from '@editorjs/model-types'; +import { BlockChildType } from '@editorjs/model-types'; +import type { InlineFragment, TextNodeSerialized } from '@editorjs/model-types'; import { TextNode } from '../inline-fragments/index.js'; import type { BlockNodeData, BlockNodeDataSerialized } from './types/index.js'; -import { BlockChildType } from './types/index.js'; -import { NODE_TYPE_HIDDEN_PROP } from './consts.js'; -import { TextAddedEvent, TuneModifiedEvent, ValueModifiedEvent } from '../../EventBus/events/index.js'; -import { EventType } from '../../EventBus/types/EventType.js'; +import { NODE_TYPE_HIDDEN_PROP } from '@editorjs/model-types'; +import { TextAddedEvent, TuneModifiedEvent, ValueModifiedEvent } from '@editorjs/model-types'; +import { EventType } from '@editorjs/model-types'; import { createBlockTuneName } from '../BlockTune/index.js'; -import { get } from '../../utils/keypath.js'; +import { get } from '@editorjs/model-types'; import { AlreadyExistingKeyError } from './errors/AlreadyExistingKeyError.js'; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed to spy on conditional-typed getter with @jest/globals strict types @@ -527,7 +528,7 @@ describe('BlockNode', () => { // microtasks before asserting that the listener was called. await Promise.resolve(); - expect(listener).toBeCalledWith(expect.objectContaining({ + expect(listener).toHaveBeenCalledWith(expect.objectContaining({ detail: { action: EventAction.Added, index: expect.objectContaining({ dataKey: key }), @@ -545,7 +546,7 @@ describe('BlockNode', () => { expect(() => { blockNode.createDataNode(key, 'another value'); - }).toThrowError(AlreadyExistingKeyError); + }).toThrow(AlreadyExistingKeyError); }); it('should create value node at a nested path within an object', () => { @@ -610,7 +611,7 @@ describe('BlockNode', () => { const existingNode = get(blockNode.data, 'meta.url'); expect(() => blockNode.createDataNode(key, 'another value')) - .toThrowError(AlreadyExistingKeyError); + .toThrow(AlreadyExistingKeyError); expect(get(blockNode.data, 'meta.url')).toStrictEqual(existingNode); }); @@ -627,7 +628,7 @@ describe('BlockNode', () => { await Promise.resolve(); - expect(listener).toBeCalledWith(expect.objectContaining({ + expect(listener).toHaveBeenCalledWith(expect.objectContaining({ detail: expect.objectContaining({ action: EventAction.Added, index: expect.objectContaining({ dataKey: key }), @@ -774,7 +775,7 @@ describe('BlockNode', () => { blockNode.removeDataNode(key); - expect(listener).toBeCalledWith(expect.objectContaining({ + expect(listener).toHaveBeenCalledWith(expect.objectContaining({ detail: { action: EventAction.Removed, index: expect.objectContaining({ dataKey: key }), @@ -832,7 +833,7 @@ describe('BlockNode', () => { blockNode.removeDataNode(key); - expect(listener).toBeCalledWith(expect.objectContaining({ + expect(listener).toHaveBeenCalledWith(expect.objectContaining({ detail: expect.objectContaining({ action: EventAction.Removed, index: expect.objectContaining({ dataKey: key }), @@ -1025,7 +1026,7 @@ describe('BlockNode', () => { expect(() => { blockNode.updateValue(dataKey, value); }) - .toThrowError(`BlockNode: data with key "${dataKey}" is not a ValueNode`); + .toThrow(`BlockNode: data with key "${dataKey}" is not a ValueNode`); }); }); diff --git a/packages/model/src/entities/BlockNode/__mocks__/index.ts b/packages/model/src/entities/BlockNode/__mocks__/index.ts index 70899bb2..c6bd9b80 100644 --- a/packages/model/src/entities/BlockNode/__mocks__/index.ts +++ b/packages/model/src/entities/BlockNode/__mocks__/index.ts @@ -1,8 +1,8 @@ -import { EventBus } from '../../../EventBus/EventBus.js'; +import { EventBus } from '@editorjs/model-types'; import { create } from '../../../utils/index.js'; -import type { DataKey } from '../types/index.js'; -import type { BlockId } from '../types/BlockId.js'; -import { createBlockId } from '../types/BlockId.js'; +import type { DataKey } from '@editorjs/model-types'; +import type { BlockId } from '@editorjs/model-types'; +import { createBlockId } from '@editorjs/model-types'; export const createDataKey = create(); diff --git a/packages/model/src/entities/BlockNode/consts.ts b/packages/model/src/entities/BlockNode/consts.ts deleted file mode 100644 index 0a14bb78..00000000 --- a/packages/model/src/entities/BlockNode/consts.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Name of hidden property to mark value and text nodes in serialized data - */ -export const NODE_TYPE_HIDDEN_PROP = '$t'; diff --git a/packages/model/src/entities/BlockNode/errors/AlreadyExistingKeyError.ts b/packages/model/src/entities/BlockNode/errors/AlreadyExistingKeyError.ts index 4764ff92..8fbae361 100644 --- a/packages/model/src/entities/BlockNode/errors/AlreadyExistingKeyError.ts +++ b/packages/model/src/entities/BlockNode/errors/AlreadyExistingKeyError.ts @@ -1,4 +1,4 @@ -import type { DataKey } from '../types/index.js'; +import type { DataKey } from '@editorjs/model-types'; /** * Error is thrown on attempt to create data with already existing key diff --git a/packages/model/src/entities/BlockNode/errors/InvalidNodeTypeError.ts b/packages/model/src/entities/BlockNode/errors/InvalidNodeTypeError.ts index 98c0dd34..ccffd623 100644 --- a/packages/model/src/entities/BlockNode/errors/InvalidNodeTypeError.ts +++ b/packages/model/src/entities/BlockNode/errors/InvalidNodeTypeError.ts @@ -1,4 +1,4 @@ -import type { DataKey } from '../types/index.js'; +import type { DataKey } from '@editorjs/model-types'; /** * diff --git a/packages/model/src/entities/BlockNode/errors/NonExistingKeyError.ts b/packages/model/src/entities/BlockNode/errors/NonExistingKeyError.ts index 54ac7899..b6744d6d 100644 --- a/packages/model/src/entities/BlockNode/errors/NonExistingKeyError.ts +++ b/packages/model/src/entities/BlockNode/errors/NonExistingKeyError.ts @@ -1,4 +1,4 @@ -import type { DataKey } from '../types/index.js'; +import type { DataKey } from '@editorjs/model-types'; /** * diff --git a/packages/model/src/entities/BlockNode/index.ts b/packages/model/src/entities/BlockNode/index.ts index 82648d86..d915e5d1 100644 --- a/packages/model/src/entities/BlockNode/index.ts +++ b/packages/model/src/entities/BlockNode/index.ts @@ -2,7 +2,6 @@ import { getContext } from '../../utils/Context.js'; import type { EditorDocument } from '../EditorDocument/index.js'; import type { BlockTuneName, BlockTuneSerialized } from '../BlockTune/index.js'; import { BlockTune, createBlockTuneName } from '../BlockTune/index.js'; -import { IndexBuilder } from '../Index/IndexBuilder.js'; import { InvalidNodeTypeError } from './errors/InvalidNodeTypeError.js'; import { NonExistingKeyError } from './errors/NonExistingKeyError.js'; import type { @@ -13,31 +12,42 @@ import type { BlockNodeDataValue, BlockNodeSerialized, BlockToolName, - ChildNode, - DataKey + ChildNode } from './types/index.js'; -import { BlockChildType, createBlockToolName, createDataKey } from './types/index.js'; -import type { BlockId } from './types/index.js'; -import { createBlockId, generateBlockId } from './types/index.js'; -import type { ValueSerialized } from '../ValueNode/index.js'; +import { createBlockToolName } from './types/index.js'; import { ValueNode } from '../ValueNode/index.js'; -import type { InlineFragment, InlineToolData, InlineToolName, TextNodeSerialized } from '../inline-fragments/index.js'; import { TextNode } from '../inline-fragments/index.js'; -import { get, has, set, remove, insert } from '../../utils/keypath.js'; -import { NODE_TYPE_HIDDEN_PROP } from './consts.js'; +import type { InlineFragment, TextNodeSerialized, ValueSerialized } from '@editorjs/model-types'; +import { + IndexBuilder, + BlockChildType, + createDataKey, + createBlockId, + generateBlockId, + get, + has, + set, + remove, + insert, + EventBus, + EventType, + BaseDocumentEvent, + type DataKey, + type BlockId, + type InlineToolData, + type InlineToolName, + type TextNodeEvents +} from '@editorjs/model-types'; +import { NODE_TYPE_HIDDEN_PROP } from '@editorjs/model-types'; import { mapObject } from '../../utils/mapObject.js'; import type { DeepReadonly } from '../../utils/DeepReadonly.js'; -import { EventBus } from '../../EventBus/EventBus.js'; -import { EventType } from '../../EventBus/types/EventType.js'; import { DataNodeRemovedEvent, DataNodeAddedEvent, TuneModifiedEvent, ValueModifiedEvent -} from '../../EventBus/events/index.js'; +} from '@editorjs/model-types'; import type { Constructor } from '../../utils/types.js'; -import type { TextNodeEvents } from '../../EventBus/types/EventMap.js'; -import { BaseDocumentEvent } from '../../EventBus/events/BaseEvent.js'; import { AlreadyExistingKeyError } from './errors/AlreadyExistingKeyError.js'; /** @@ -622,14 +632,10 @@ export type { BlockId }; -export type { BlockNodeInit, BlockIndexOrId } from './types/index.js'; +export type { BlockNodeInit, BlockIndexOrId } from '@editorjs/model-types'; export { - createBlockToolName, - createDataKey, - createBlockId, - generateBlockId + createBlockToolName }; -export { NODE_TYPE_HIDDEN_PROP } from './consts.js'; -export { BlockChildType }; +export { createDataKey, createBlockId, generateBlockId, NODE_TYPE_HIDDEN_PROP, BlockChildType } from '@editorjs/model-types'; diff --git a/packages/model/src/entities/BlockNode/types/BlockChildType.ts b/packages/model/src/entities/BlockNode/types/BlockChildType.ts deleted file mode 100644 index 20d5deff..00000000 --- a/packages/model/src/entities/BlockNode/types/BlockChildType.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum BlockChildType { - Value = 'v', - Text = 't' -} diff --git a/packages/model/src/entities/BlockNode/types/BlockId.ts b/packages/model/src/entities/BlockNode/types/BlockId.ts deleted file mode 100644 index 87bd8036..00000000 --- a/packages/model/src/entities/BlockNode/types/BlockId.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Nominal } from '../../../utils/Nominal.js'; -import { create } from '../../../utils/Nominal.js'; - -/** - * Base type of the block node identifier field - */ -type BlockIdBase = string; - -/** - * Nominal type for a unique block identifier - */ -export type BlockId = Nominal; - -/** - * Accepts either a numeric block index or a block's unique string identifier. - * Pass a number to address a block by position; pass a BlockId to address by id. - */ -export type BlockIndexOrId = number | BlockId; - -/** - * Function returns a value with the nominal BlockId type - */ -export const createBlockId = create(); - -/** - * URL-safe alphabet used for ID generation - */ -// Stryker disable next-line StringLiteral -const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'; - -/** - * Length of generated block IDs (21 chars ≈ 126 bits of randomness, matching nanoid defaults) - */ -const ID_LENGTH = 21; - -/** - * Generates a new unique BlockId — a 21-character URL-safe random string - */ -export const generateBlockId = (): BlockId => { - const bytes = crypto.getRandomValues(new Uint8Array(ID_LENGTH)); - - /* Stryker disable next-line ArithmeticOperator -- bitwise mask is intentional, not an arithmetic mutation target */ - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - return createBlockId(Array.from(bytes, byte => ALPHABET[byte & 63]).join('')); -}; diff --git a/packages/model/src/entities/BlockNode/types/BlockNodeConstructorParameters.ts b/packages/model/src/entities/BlockNode/types/BlockNodeConstructorParameters.ts index 902c8827..db09112b 100644 --- a/packages/model/src/entities/BlockNode/types/BlockNodeConstructorParameters.ts +++ b/packages/model/src/entities/BlockNode/types/BlockNodeConstructorParameters.ts @@ -1,7 +1,7 @@ import type { EditorDocument } from '../../EditorDocument/index.js'; import type { BlockTuneSerialized } from '../../BlockTune/index.js'; import type { BlockNodeDataSerialized } from './BlockNodeSerialized.js'; -import type { BlockId } from './BlockId.js'; +import type { BlockId } from '@editorjs/model-types'; export interface BlockNodeConstructorParameters { /** diff --git a/packages/model/src/entities/BlockNode/types/BlockNodeData.ts b/packages/model/src/entities/BlockNode/types/BlockNodeData.ts index 2021a88d..27f8874f 100644 --- a/packages/model/src/entities/BlockNode/types/BlockNodeData.ts +++ b/packages/model/src/entities/BlockNode/types/BlockNodeData.ts @@ -1,4 +1,4 @@ -import type { DataKey } from './DataKey.js'; +import type { DataKey } from '@editorjs/model-types'; import type { ValueNode } from '../../ValueNode/index.js'; import type { TextNode } from '../../inline-fragments/index.js'; diff --git a/packages/model/src/entities/BlockNode/types/BlockNodeSerialized.ts b/packages/model/src/entities/BlockNode/types/BlockNodeSerialized.ts index 8796a300..0e0b185f 100644 --- a/packages/model/src/entities/BlockNode/types/BlockNodeSerialized.ts +++ b/packages/model/src/entities/BlockNode/types/BlockNodeSerialized.ts @@ -1,53 +1,8 @@ -import type { BlockTuneSerialized } from '../../BlockTune/index.js'; -import type { ValueSerialized } from '../../ValueNode/types/ValueSerialized.js'; -import type { TextNodeSerialized } from '../../inline-fragments/index.js'; -import type { BlockId } from './BlockId.js'; - -/** - * Union type of serialized BlockNode child nodes - */ -export type BlockChildNodeSerialized = ValueSerialized | TextNodeSerialized; - -/** - * Reccurrent type representing serialized BlockNode data - */ -export type BlockNodeDataSerializedValue = BlockChildNodeSerialized | BlockChildNodeSerialized[] | BlockNodeDataSerialized | BlockNodeDataSerialized[]; - -/** - * Root type representing serialized BlockNode data - */ -export interface BlockNodeDataSerialized { - [key: string]: BlockNodeDataSerializedValue; -} - -/** - * Serialized version of the BlockNode - */ -export interface BlockNodeSerialized { - /** - * Unique identifier of the Block - */ - id: string; - - /** - * The name of the tool created a Block - */ - name: string; - - /** - * The content of the Block - */ - data: BlockNodeDataSerialized; - - /** - * Serialized BlockTunes associated with the BlockNode - */ - tunes?: Record; -} - -/** - * Input type for creating a BlockNode. - * Only `name` is required — all other fields (including `id`) are optional. - * A unique id will be auto-generated when omitted. - */ -export type BlockNodeInit = { name: string } & Partial>; +export type { + BlockChildNodeSerialized, + BlockNodeDataSerializedValue, + BlockNodeDataSerialized, + BlockData, + BlockNodeSerialized, + BlockNodeInit +} from '@editorjs/model-types'; diff --git a/packages/model/src/entities/BlockNode/types/BlockToolName.ts b/packages/model/src/entities/BlockNode/types/BlockToolName.ts deleted file mode 100644 index 9f59ae66..00000000 --- a/packages/model/src/entities/BlockNode/types/BlockToolName.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Nominal } from '../../../utils/Nominal.js'; -import { create } from '../../../utils/Nominal.js'; - -/** - * Base type of the block node name field - */ -type BlockToolNameBase = string; - -/** - * Nominal type for the block node name field - */ -export type BlockToolName = Nominal; - -/** - * Function returns a value with the nominal BlockToolName type - */ -export const createBlockToolName = create(); diff --git a/packages/model/src/entities/BlockNode/types/DataKey.ts b/packages/model/src/entities/BlockNode/types/DataKey.ts deleted file mode 100644 index d3c380e5..00000000 --- a/packages/model/src/entities/BlockNode/types/DataKey.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Nominal } from '../../../utils/Nominal.js'; -import { create } from '../../../utils/Nominal.js'; - -/** - * Base type of the data key field - * - * DataKeyBase is a string for object properties or a number for array indexes - * DataKey could be a compound key path, e.g. 'dataKey.nestedArrayKey.0' - */ -type DataKeyBase = string | number; - -/** - * Nominal type for the data key field - */ -export type DataKey = Nominal; - -/** - * Function returns a value with the nominal DataKey type - */ -export const createDataKey = create(); diff --git a/packages/model/src/entities/BlockNode/types/index.ts b/packages/model/src/entities/BlockNode/types/index.ts index 6c0e5e05..ebcbf93a 100644 --- a/packages/model/src/entities/BlockNode/types/index.ts +++ b/packages/model/src/entities/BlockNode/types/index.ts @@ -1,10 +1,9 @@ export type { BlockNodeConstructorParameters } from './BlockNodeConstructorParameters.js'; -export type { BlockToolName } from './BlockToolName.js'; -export { createBlockToolName } from './BlockToolName.js'; -export type { BlockId, BlockIndexOrId } from './BlockId.js'; -export { createBlockId, generateBlockId } from './BlockId.js'; -export type { DataKey } from './DataKey.js'; -export { createDataKey } from './DataKey.js'; +export type { BlockToolName } from '@editorjs/model-types'; +export { createBlockToolName } from '@editorjs/model-types'; +export type { BlockId, BlockIndexOrId } from '@editorjs/model-types'; +export { createBlockId, generateBlockId } from '@editorjs/model-types'; +export type { DataKey } from '@editorjs/model-types'; +export { createDataKey } from '@editorjs/model-types'; export type { BlockNodeData, ChildNode, BlockNodeDataValue } from './BlockNodeData.js'; export type { BlockNodeSerialized, BlockChildNodeSerialized, BlockNodeDataSerialized, BlockNodeDataSerializedValue, BlockNodeInit } from './BlockNodeSerialized.js'; -export { BlockChildType } from './BlockChildType.js'; diff --git a/packages/model/src/entities/BlockTune/BlockTune.spec.ts b/packages/model/src/entities/BlockTune/BlockTune.spec.ts index 38ab658d..8a210912 100644 --- a/packages/model/src/entities/BlockTune/BlockTune.spec.ts +++ b/packages/model/src/entities/BlockTune/BlockTune.spec.ts @@ -1,7 +1,7 @@ import { BlockTune, createBlockTuneName } from './index.js'; -import { EventType } from '../../EventBus/types/EventType.js'; -import { TuneModifiedEvent } from '../../EventBus/events/index.js'; -import { EventAction } from '../../EventBus/types/EventAction.js'; +import { EventType } from '@editorjs/model-types'; +import { TuneModifiedEvent } from '@editorjs/model-types'; +import { EventAction } from '@editorjs/model-types'; describe('BlockTune', () => { const tuneName = createBlockTuneName('alignment'); diff --git a/packages/model/src/entities/BlockTune/__mocks__/index.ts b/packages/model/src/entities/BlockTune/__mocks__/index.ts index 7c4b03fd..ba0a21d8 100644 --- a/packages/model/src/entities/BlockTune/__mocks__/index.ts +++ b/packages/model/src/entities/BlockTune/__mocks__/index.ts @@ -1,5 +1,5 @@ import { createBlockTuneName } from '../types/index.js'; -import { EventBus } from '../../../EventBus/EventBus.js'; +import { EventBus } from '@editorjs/model-types'; /** * Mock for BlockTune class diff --git a/packages/model/src/entities/BlockTune/index.ts b/packages/model/src/entities/BlockTune/index.ts index 5075d5a8..c7cfddf7 100644 --- a/packages/model/src/entities/BlockTune/index.ts +++ b/packages/model/src/entities/BlockTune/index.ts @@ -1,9 +1,9 @@ import { getContext } from '../../utils/Context.js'; -import { IndexBuilder } from '../Index/IndexBuilder.js'; +import { IndexBuilder } from '@editorjs/model-types'; import type { BlockTuneConstructorParameters, BlockTuneSerialized, BlockTuneName } from './types/index.js'; import { createBlockTuneName } from './types/index.js'; -import { EventBus } from '../../EventBus/EventBus.js'; -import { TuneModifiedEvent } from '../../EventBus/events/index.js'; +import { EventBus } from '@editorjs/model-types'; +import { TuneModifiedEvent } from '@editorjs/model-types'; /** * BlockTune class represents a set of additional information associated with a BlockNode. diff --git a/packages/model/src/entities/BlockTune/types/BlockTuneConstructorParameters.ts b/packages/model/src/entities/BlockTune/types/BlockTuneConstructorParameters.ts index 058da9ac..b0a1719b 100644 --- a/packages/model/src/entities/BlockTune/types/BlockTuneConstructorParameters.ts +++ b/packages/model/src/entities/BlockTune/types/BlockTuneConstructorParameters.ts @@ -1,5 +1,4 @@ -import type { BlockTuneName } from './BlockTuneName.js'; -import type { BlockTuneSerialized } from './BlockTuneSerialized.js'; +import type { BlockTuneName, BlockTuneSerialized } from '@editorjs/model-types'; export interface BlockTuneConstructorParameters { /** diff --git a/packages/model/src/entities/BlockTune/types/BlockTuneName.ts b/packages/model/src/entities/BlockTune/types/BlockTuneName.ts deleted file mode 100644 index 59810638..00000000 --- a/packages/model/src/entities/BlockTune/types/BlockTuneName.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Nominal } from '../../../utils/Nominal.js'; -import { create } from '../../../utils/Nominal.js'; - -/** - * Base type of the block tune name field - */ -type BlockTuneNameBase = string; - -/** - * Nominal type for the block tune name field - */ -export type BlockTuneName = Nominal; - -/** - * Function returns a value with the nominal BlockTuneName type - */ -export const createBlockTuneName = create(); diff --git a/packages/model/src/entities/BlockTune/types/BlockTuneSerialized.ts b/packages/model/src/entities/BlockTune/types/BlockTuneSerialized.ts deleted file mode 100644 index 8892f4f1..00000000 --- a/packages/model/src/entities/BlockTune/types/BlockTuneSerialized.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * BlockTuneSerialized represents a serialized version of a BlockTune. - */ -export type BlockTuneSerialized = Record; diff --git a/packages/model/src/entities/BlockTune/types/index.ts b/packages/model/src/entities/BlockTune/types/index.ts index 5907be68..fb3c61e7 100644 --- a/packages/model/src/entities/BlockTune/types/index.ts +++ b/packages/model/src/entities/BlockTune/types/index.ts @@ -1,4 +1,4 @@ export type { BlockTuneConstructorParameters } from './BlockTuneConstructorParameters.js'; -export type { BlockTuneName } from './BlockTuneName.js'; -export { createBlockTuneName } from './BlockTuneName.js'; -export type { BlockTuneSerialized } from './BlockTuneSerialized.js'; +export type { BlockTuneName } from '@editorjs/model-types'; +export { createBlockTuneName } from '@editorjs/model-types'; +export type { BlockTuneSerialized } from '@editorjs/model-types'; diff --git a/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts b/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts index 05253e73..4b652842 100644 --- a/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts +++ b/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts @@ -1,18 +1,16 @@ -import { IndexBuilder } from '../Index/IndexBuilder.js'; +import { IndexBuilder } from '@editorjs/model-types'; import { EditorDocument } from './index.js'; -import type { BlockToolName, DataKey } from '../BlockNode/index.js'; +import { createBlockId, type DataKey, type InlineToolData, type InlineToolName, type BlockToolName } from '@editorjs/model-types'; import { BlockNode } from '../BlockNode/index.js'; -import { createBlockId } from '../BlockNode/types/BlockId.js'; import type { BlockTuneName } from '../BlockTune/index.js'; -import type { InlineToolData, InlineToolName } from '../inline-fragments/index.js'; -import { EventType } from '../../EventBus/types/EventType.js'; +import { EventType } from '@editorjs/model-types'; import { BlockAddedEvent, BlockRemovedEvent, PropertyModifiedEvent, TuneModifiedEvent -} from '../../EventBus/events/index.js'; -import { EventAction } from '../../EventBus/types/EventAction.js'; +} from '@editorjs/model-types'; +import { EventAction } from '@editorjs/model-types'; import { describe, jest } from '@jest/globals'; import { BlockAlreadyExistsError } from './errors/BlockAlreadyExistsError.js'; @@ -304,7 +302,7 @@ describe('EditorDocument', () => { // Assert expect(action) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); it('should throw an error if index is greater then document length', () => { @@ -319,7 +317,7 @@ describe('EditorDocument', () => { // Assert expect(action) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); it('should throw BlockAlreadyExistsError when a block with the same id is added twice', () => { @@ -416,7 +414,7 @@ describe('EditorDocument', () => { // Assert expect(action) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); it('should throw an error if index is greater then document length', () => { @@ -428,7 +426,7 @@ describe('EditorDocument', () => { // Assert expect(action) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); it('should emit BlockRemovedEvent with block node data in details and block index', () => { @@ -494,7 +492,7 @@ describe('EditorDocument', () => { // Assert expect(action) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); it('should throw an error if index is greater then document length', () => { @@ -506,7 +504,7 @@ describe('EditorDocument', () => { // Assert expect(action) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); }); @@ -557,7 +555,7 @@ describe('EditorDocument', () => { const nonExistentId = createBlockId('ghost'); expect(() => document.removeBlock(nonExistentId)) - .toThrowError(`Block with id "${nonExistentId}" not found`); + .toThrow(`Block with id "${nonExistentId}" not found`); }); }); @@ -771,7 +769,7 @@ describe('EditorDocument', () => { const action = (): void => document.createDataNode(blockIndexOutOfBound, dataKey, expectedValue); expect(action) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); }); @@ -877,7 +875,7 @@ describe('EditorDocument', () => { }; expect(action) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); }); @@ -985,7 +983,7 @@ describe('EditorDocument', () => { const action = (): void => document.removeDataNode(blockIndexOutOfBound, dataKey); expect(action) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); }); @@ -1096,7 +1094,7 @@ describe('EditorDocument', () => { const action = (): void => document.updateValue(blockIndexOutOfBound, dataKey, expectedValue); expect(action) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); }); @@ -1213,7 +1211,7 @@ describe('EditorDocument', () => { const action = (): void => document.updateTuneData(blockIndexOutOfBound, tuneName, updateData); expect(action) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); }); @@ -1306,7 +1304,7 @@ describe('EditorDocument', () => { it('should throw an error if index is out of bounds', () => { expect(() => document.insertText(document.length + 1, dataKey, text)) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); }); @@ -1584,7 +1582,7 @@ describe('EditorDocument', () => { it('should throw an error if index is out of bounds', () => { expect(() => document.removeText(document.length + 1, dataKey)) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); }); @@ -1638,7 +1636,7 @@ describe('EditorDocument', () => { it('should throw an error if index is out of bounds', () => { expect(() => document.format(document.length + 1, dataKey, tool, start, end)) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); }); @@ -1682,7 +1680,7 @@ describe('EditorDocument', () => { it('should throw an error if index is out of bounds', () => { expect(() => document.unformat(document.length + 1, dataKey, tool, start, end)) - .toThrowError('Index out of bounds'); + .toThrow('Index out of bounds'); }); }); @@ -1698,7 +1696,7 @@ describe('EditorDocument', () => { document.serialized; expect(spy) - .toBeCalledTimes(document.length); + .toHaveBeenCalledTimes(document.length); }); it('should return document properties', () => { diff --git a/packages/model/src/entities/EditorDocument/errors/BlockAlreadyExistsError.ts b/packages/model/src/entities/EditorDocument/errors/BlockAlreadyExistsError.ts index e679cf38..22b604af 100644 --- a/packages/model/src/entities/EditorDocument/errors/BlockAlreadyExistsError.ts +++ b/packages/model/src/entities/EditorDocument/errors/BlockAlreadyExistsError.ts @@ -1,4 +1,4 @@ -import type { BlockId } from '../../BlockNode/types/BlockId.js'; +import type { BlockId } from '@editorjs/model-types'; /** * Error thrown when attempting to add a block whose id already exists in the document. diff --git a/packages/model/src/entities/EditorDocument/index.ts b/packages/model/src/entities/EditorDocument/index.ts index cf12e48f..be487143 100644 --- a/packages/model/src/entities/EditorDocument/index.ts +++ b/packages/model/src/entities/EditorDocument/index.ts @@ -1,40 +1,38 @@ -import type { DocumentId } from '../../EventBus/index.js'; import { getContext } from '../../utils/Context.js'; -import { createDataKey, type DataKey } from '../BlockNode/index.js'; import { BlockNode } from '../BlockNode/index.js'; -import type { BlockId } from '../BlockNode/index.js'; -import type { BlockIndexOrId } from '../BlockNode/index.js'; -import { IndexBuilder } from '../Index/IndexBuilder.js'; -import type { EditorDocumentSerialized, EditorDocumentConstructorParameters, Properties } from './types/index.js'; -import type { BlockTuneName } from '../BlockTune/index.js'; -import type { - TextNodeSerialized -} from '../inline-fragments/index.js'; import { - type InlineFragment, + createDataKey, + IndexBuilder, + EventBus, + EventType, + type DocumentId, + type DataKey, + type BlockId, + type BlockIndexOrId, + type Properties, type InlineToolData, - type InlineToolName -} from '../inline-fragments/index.js'; + type InlineToolName, + type BlockTuneEvents, + type TextNodeEvents, + type ValueNodeEvents +} from '@editorjs/model-types'; +import type { EditorDocumentSerialized } from './types/index.js'; +import type { EditorDocumentConstructorParameters } from './types/index.js'; +import type { BlockTuneName } from '../BlockTune/index.js'; +import type { TextNodeSerialized, InlineFragment, ValueSerialized } from '@editorjs/model-types'; import { IoCContainer, TOOLS_REGISTRY } from '../../IoC/index.js'; import { ToolsRegistry } from '../../tools/index.js'; import type { BlockNodeDataSerializedValue, BlockNodeSerialized, BlockNodeInit } from '../BlockNode/types/index.js'; import type { DeepReadonly } from '../../utils/DeepReadonly.js'; -import { EventBus } from '../../EventBus/EventBus.js'; -import { EventType } from '../../EventBus/types/EventType.js'; -import type { - BlockTuneEvents, - TextNodeEvents, - ValueNodeEvents -} from '../../EventBus/types/EventMap.js'; import { BlockAddedEvent, BlockRemovedEvent, - PropertyModifiedEvent, type TextFormattedEventData, type TextUnformattedEventData -} from '../../EventBus/events/index.js'; + PropertyModifiedEvent, + TuneModifiedEvent +} from '@editorjs/model-types'; import type { Constructor } from '../../utils/types.js'; -import { BaseDocumentEvent, type ModifiedEventData } from '../../EventBus/events/BaseEvent.js'; -import type { Index } from '../Index/index.js'; -import type { ValueSerialized } from '../ValueNode/index.js'; +import { BaseDocumentEvent, type ModifiedEventData, type TextFormattedEventData, type TextUnformattedEventData } from '@editorjs/model-types'; +import type { Index } from '@editorjs/model-types'; import { BlockAlreadyExistsError } from './errors/BlockAlreadyExistsError.js'; export type * from './types/index.js'; diff --git a/packages/model/src/entities/EditorDocument/types/EditorDocumentConstructorParameters.ts b/packages/model/src/entities/EditorDocument/types/EditorDocumentConstructorParameters.ts index d568fd82..6b192eb9 100644 --- a/packages/model/src/entities/EditorDocument/types/EditorDocumentConstructorParameters.ts +++ b/packages/model/src/entities/EditorDocument/types/EditorDocumentConstructorParameters.ts @@ -1,4 +1,4 @@ -import type { Properties } from './Properties.js'; +import type { Properties } from '@editorjs/model-types'; import type { ToolsRegistry } from '../../../tools/ToolsRegistry.js'; export interface EditorDocumentConstructorParameters { diff --git a/packages/model/src/entities/EditorDocument/types/EditorDocumentSerialized.ts b/packages/model/src/entities/EditorDocument/types/EditorDocumentSerialized.ts deleted file mode 100644 index 88e77985..00000000 --- a/packages/model/src/entities/EditorDocument/types/EditorDocumentSerialized.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { BlockNodeSerialized } from '../../BlockNode/types/index.js'; -import type { Properties } from './Properties.js'; - -/** - * Type representing serialized EditorDocument - * - * Serialized EditorDocument is a JSON object containing blocks and document properties - */ -export interface EditorDocumentSerialized { - /** - * Document identifier - */ - identifier: string; - - /** - * Array of serialized BlockNodes - */ - blocks: BlockNodeSerialized[]; - - /** - * JSON object containing document properties (eg read-only) - */ - properties: Properties; -} diff --git a/packages/model/src/entities/EditorDocument/types/Properties.ts b/packages/model/src/entities/EditorDocument/types/Properties.ts deleted file mode 100644 index 4e37c820..00000000 --- a/packages/model/src/entities/EditorDocument/types/Properties.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Alias for EditorDocument properties type - */ -export type Properties = Record; diff --git a/packages/model/src/entities/EditorDocument/types/index.ts b/packages/model/src/entities/EditorDocument/types/index.ts index 1677874b..0c5f6aca 100644 --- a/packages/model/src/entities/EditorDocument/types/index.ts +++ b/packages/model/src/entities/EditorDocument/types/index.ts @@ -1,4 +1,3 @@ export type { EditorDocumentConstructorParameters } from './EditorDocumentConstructorParameters.js'; -export type { Properties } from './Properties.js'; +export type { Properties, EditorDocumentSerialized } from '@editorjs/model-types'; export type { BlockNodeData } from './BlockNodeData.js'; -export type { EditorDocumentSerialized } from './EditorDocumentSerialized.js'; diff --git a/packages/model/src/entities/Index/IndexBuilder.ts b/packages/model/src/entities/Index/IndexBuilder.ts deleted file mode 100644 index 1cadaad3..00000000 --- a/packages/model/src/entities/Index/IndexBuilder.ts +++ /dev/null @@ -1,116 +0,0 @@ -import type { DocumentIndex, TextRange } from '../../EventBus/index.js'; -import type { DataKey } from '../BlockNode/index.js'; -import type { BlockTuneName } from '../BlockTune/index.js'; -import { Index } from './index.js'; - -/** - * - */ -export class IndexBuilder { - #index = new Index(); - - /** - * Add text range to the index - * @param range - text range - */ - public addTextRange(range?: TextRange): this { - this.#index.textRange = range; - - return this; - } - - /** - * Add data key to the index - * @param key - data key - */ - public addDataKey(key?: DataKey): this { - this.#index.dataKey = key; - - return this; - } - - /** - * Add tune key to the index - * @param key - tune key - */ - public addTuneKey(key?: string): this { - this.#index.tuneKey = key; - - return this; - } - - /** - * Add tune name to the index - * @param name - tune name - */ - public addTuneName(name?: BlockTuneName): this { - this.#index.tuneName = name; - - return this; - } - - /** - * Add block index to the index - * @param index - block index - */ - public addBlockIndex(index?: number): this { - this.#index.blockIndex = index; - - return this; - } - - /** - * Add property name to the index - * @param name - property name - */ - public addPropertyName(name?: string): this { - this.#index.propertyName = name; - - return this; - } - - /** - * Add document id to the index - * @param id - document id - */ - public addDocumentId(id?: DocumentIndex): this { - this.#index.documentId = id; - - return this; - } - - /** - * Returns the index object - */ - public build(): Index { - this.#index.validate(); - - return this.#index; - } - - /** - * Set index from serialized index - * @param json - serialized index - */ - public from(json: string): this; - /** - * Set index from index object - * @param index - index object - */ - public from(index: Index): this; - /** - * Set index from index object or serialized index - * @param indexOrJSON - index object or serialized index - */ - public from(indexOrJSON: Index | string): this { - if (typeof indexOrJSON === 'string') { - this.#index = Index.parse(indexOrJSON); - - return this; - } - - this.#index = indexOrJSON.clone(); - - return this; - } -} diff --git a/packages/model/src/entities/ValueNode/ValueNode.spec.ts b/packages/model/src/entities/ValueNode/ValueNode.spec.ts index f14fabec..ef16b5d8 100644 --- a/packages/model/src/entities/ValueNode/ValueNode.spec.ts +++ b/packages/model/src/entities/ValueNode/ValueNode.spec.ts @@ -1,10 +1,10 @@ -import { Index } from '../Index/index.js'; +import { Index } from '@editorjs/model-types'; import { ValueNode } from './index.js'; -import { BlockChildType } from '../BlockNode/types/index.js'; -import { NODE_TYPE_HIDDEN_PROP } from '../BlockNode/consts.js'; -import { EventType } from '../../EventBus/types/EventType.js'; -import { ValueModifiedEvent } from '../../EventBus/events/index.js'; -import { EventAction } from '../../EventBus/types/EventAction.js'; +import { BlockChildType } from '@editorjs/model-types'; +import { NODE_TYPE_HIDDEN_PROP } from '@editorjs/model-types'; +import { EventType } from '@editorjs/model-types'; +import { ValueModifiedEvent } from '@editorjs/model-types'; +import { EventAction } from '@editorjs/model-types'; describe('ValueNode', () => { describe('.update()', () => { @@ -35,7 +35,7 @@ describe('ValueNode', () => { longitudeValueNode.update(updatedLongitude); - expect(handler).toBeCalledWith(expect.any(ValueModifiedEvent)); + expect(handler).toHaveBeenCalledWith(expect.any(ValueModifiedEvent)); }); it('should emit ValueModifiedEvent with correct details', () => { diff --git a/packages/model/src/entities/ValueNode/__mocks__/index.ts b/packages/model/src/entities/ValueNode/__mocks__/index.ts index 7ac4d1f8..0bb4d402 100644 --- a/packages/model/src/entities/ValueNode/__mocks__/index.ts +++ b/packages/model/src/entities/ValueNode/__mocks__/index.ts @@ -1,4 +1,4 @@ -import { EventBus } from '../../../EventBus/EventBus.js'; +import { EventBus } from '@editorjs/model-types'; /** * Mock for ValueNode class diff --git a/packages/model/src/entities/ValueNode/index.ts b/packages/model/src/entities/ValueNode/index.ts index a5189f59..d1ff4a5c 100644 --- a/packages/model/src/entities/ValueNode/index.ts +++ b/packages/model/src/entities/ValueNode/index.ts @@ -1,10 +1,11 @@ import { getContext } from '../../utils/Context.js'; -import { IndexBuilder } from '../Index/IndexBuilder.js'; -import type { ValueNodeConstructorParameters, ValueSerialized } from './types/index.js'; -import { BlockChildType } from '../BlockNode/types/index.js'; -import { NODE_TYPE_HIDDEN_PROP } from '../BlockNode/consts.js'; -import { EventBus } from '../../EventBus/EventBus.js'; -import { ValueModifiedEvent } from '../../EventBus/events/ValueModifiedEvent.js'; +import { IndexBuilder } from '@editorjs/model-types'; +import type { ValueNodeConstructorParameters } from './types/index.js'; +import type { ValueSerialized } from '@editorjs/model-types'; +import { BlockChildType } from '@editorjs/model-types'; +import { NODE_TYPE_HIDDEN_PROP } from '@editorjs/model-types'; +import { EventBus } from '@editorjs/model-types'; +import { ValueModifiedEvent } from '@editorjs/model-types'; /** * ValueNode class represents a node in a tree-like structure, used to store and manipulate data associated with a BlockNode. @@ -69,6 +70,5 @@ export class ValueNode extends EventBus { } export type { - ValueNodeConstructorParameters, - ValueSerialized + ValueNodeConstructorParameters }; diff --git a/packages/model/src/entities/ValueNode/types/ValueSerialized.ts b/packages/model/src/entities/ValueNode/types/ValueSerialized.ts deleted file mode 100644 index deab841c..00000000 --- a/packages/model/src/entities/ValueNode/types/ValueSerialized.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { BlockChildType } from '../../BlockNode/types/index.js'; -import type { NODE_TYPE_HIDDEN_PROP } from '../../BlockNode/consts.js'; - -/** - * Type representing serialized ValueNode - * - * Serialized ValueNode is a JSON primitive value or a JSON object with hidden property $t - */ -export type ValueSerialized = V extends Record ? V & { [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Value } : V; diff --git a/packages/model/src/entities/ValueNode/types/index.ts b/packages/model/src/entities/ValueNode/types/index.ts index 39e12299..07390a81 100644 --- a/packages/model/src/entities/ValueNode/types/index.ts +++ b/packages/model/src/entities/ValueNode/types/index.ts @@ -1,2 +1 @@ export type { ValueNodeConstructorParameters } from './ValueNodeConstructorParameters.js'; -export type { ValueSerialized } from './ValueSerialized.js'; diff --git a/packages/model/src/entities/index.ts b/packages/model/src/entities/index.ts index 15416ae6..0db7aa2c 100644 --- a/packages/model/src/entities/index.ts +++ b/packages/model/src/entities/index.ts @@ -3,5 +3,3 @@ export * from './BlockNode/index.js'; export * from './BlockTune/index.js'; export * from './ValueNode/index.js'; export * from './inline-fragments/index.js'; -export * from './Index/index.js'; -export * from './Index/IndexBuilder.js'; diff --git a/packages/model/src/entities/inline-fragments/FormattingInlineNode/__mocks__/index.ts b/packages/model/src/entities/inline-fragments/FormattingInlineNode/__mocks__/index.ts index 071adc2f..3b5215bd 100644 --- a/packages/model/src/entities/inline-fragments/FormattingInlineNode/__mocks__/index.ts +++ b/packages/model/src/entities/inline-fragments/FormattingInlineNode/__mocks__/index.ts @@ -1,8 +1,6 @@ import { ParentNode } from '../../mixins/ParentNode/index.js'; import { ChildNode } from '../../mixins/ChildNode/index.js'; -import type { InlineFragment } from '../../InlineNode/index.js'; - -export * from '../types/InlineToolData.js'; +import type { InlineFragment } from '@editorjs/model-types'; export interface FormattingInlineNode extends ParentNode, ChildNode {} diff --git a/packages/model/src/entities/inline-fragments/FormattingInlineNode/index.ts b/packages/model/src/entities/inline-fragments/FormattingInlineNode/index.ts index f40a26eb..81e36627 100644 --- a/packages/model/src/entities/inline-fragments/FormattingInlineNode/index.ts +++ b/packages/model/src/entities/inline-fragments/FormattingInlineNode/index.ts @@ -1,14 +1,13 @@ import type { - FormattingInlineNodeConstructorParameters, - InlineToolName, - InlineToolData + FormattingInlineNodeConstructorParameters } from './types/index.js'; -import type { InlineFragment, InlineNode } from '../InlineNode/index.js'; +import type { InlineToolName, InlineToolData, InlineFragment } from '@editorjs/model-types'; +import type { InlineNode } from '../InlineNode/index.js'; import { ParentNode } from '../mixins/ParentNode/index.js'; import { ChildNode } from '../mixins/ChildNode/index.js'; import { ParentInlineNode } from '../ParentInlineNode/index.js'; -export * from './types/index.js'; +export type * from './types/index.js'; /** * We need to extend FormattingInlineNode interface with ChildNode and ParentNode ones to use the methods from mixins diff --git a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/FormattingAction.ts b/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/FormattingAction.ts deleted file mode 100644 index f06b3e97..00000000 --- a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/FormattingAction.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Action that depends on current tool state - * Inline tool determines formatting action type and sends it to model, which updates data respectfully - */ -export enum FormattingAction { - /** - * Apply formatting for selction - */ - Format = 'format', - - /** - * Delete formatting for selection - */ - Unformat = 'unformat' -} diff --git a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/FormattingInlineNodeConstructorParameters.ts b/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/FormattingInlineNodeConstructorParameters.ts index 26ec2f27..4fcf7032 100644 --- a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/FormattingInlineNodeConstructorParameters.ts +++ b/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/FormattingInlineNodeConstructorParameters.ts @@ -1,5 +1,5 @@ -import type { InlineToolName } from './InlineToolName.js'; -import type { InlineToolData } from './InlineToolData.js'; +import type { InlineToolName } from '@editorjs/model-types'; +import type { InlineToolData } from '@editorjs/model-types'; import type { ChildNodeConstructorOptions } from '../../mixins/ChildNode/index.js'; import type { ParentNodeConstructorOptions } from '../../mixins/ParentNode/index.js'; diff --git a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/InlineToolData.ts b/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/InlineToolData.ts deleted file mode 100644 index d262a0d6..00000000 --- a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/InlineToolData.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Nominal } from '../../../../utils/Nominal.js'; -import { create } from '../../../../utils/Nominal.js'; - -/** - * Base type for Inline Tool data - */ -type InlineToolDataBase = Record; - -/** - * Nominal type for Inline Tool data - */ -export type InlineToolData = Nominal; - -/** - * Function to cast values to InlineToolData type - */ -export const createInlineToolData = create(); diff --git a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/InlineToolName.ts b/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/InlineToolName.ts deleted file mode 100644 index caefa16f..00000000 --- a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/InlineToolName.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Nominal } from '../../../../utils/Nominal.js'; -import { create } from '../../../../utils/Nominal.js'; - -/** - * Base type of the formatting node tool field - */ -type InlineToolNameBase = string; - -/** - * Nominal type for the formatting node tool field - */ -export type InlineToolName = Nominal; - -/** - * Function returns a value with the nominal InlineToolName type - */ -export const createInlineToolName = create(); diff --git a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/IntersectType.ts b/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/IntersectType.ts deleted file mode 100644 index 2b422773..00000000 --- a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/IntersectType.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Types of interaction between two inline fragments with intersect ranges - */ -export enum IntersectType { - /** - * If two fragments of one tool intersect - merge into one fragment - */ - Extend = 'extend', - - /** - * If two new fragment intersect with existing - remove existing with new one - */ - Replace = 'replace', - - /** - * If two fragments of one tool intersect - treat them as two different fragments - */ - LeaveBoth = 'leave-both' -} diff --git a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/index.ts b/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/index.ts index c42f1c76..4d044d6f 100644 --- a/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/index.ts +++ b/packages/model/src/entities/inline-fragments/FormattingInlineNode/types/index.ts @@ -1,7 +1 @@ export type { FormattingInlineNodeConstructorParameters } from './FormattingInlineNodeConstructorParameters.js'; -export type { InlineToolName } from './InlineToolName.js'; -export { createInlineToolName } from './InlineToolName.js'; -export type { InlineToolData } from './InlineToolData.js'; -export { createInlineToolData } from './InlineToolData.js'; -export { IntersectType } from './IntersectType.js'; -export { FormattingAction } from './FormattingAction.js'; diff --git a/packages/model/src/entities/inline-fragments/InlineNode/index.ts b/packages/model/src/entities/inline-fragments/InlineNode/index.ts index 179aedb0..bc29d65c 100644 --- a/packages/model/src/entities/inline-fragments/InlineNode/index.ts +++ b/packages/model/src/entities/inline-fragments/InlineNode/index.ts @@ -1,6 +1,6 @@ -import type { InlineToolData, InlineToolName } from '../FormattingInlineNode/index.js'; -import type { BlockChildType } from '../../BlockNode/types/index.js'; -import type { NODE_TYPE_HIDDEN_PROP } from '../../BlockNode/consts.js'; +import type { InlineToolData, InlineToolName } from '@editorjs/model-types'; +import type { InlineTreeNodeSerialized } from '@editorjs/model-types'; +import type { InlineFragment, TextNodeSerialized } from '@editorjs/model-types'; /** * Interface describing abstract InlineNode — common properties and methods for all inline nodes @@ -91,49 +91,3 @@ export interface InlineNode { */ normalize(): void; } - -/** - * Serialized inline fragment - */ -export interface InlineFragment { - /** - * Name of the applied Inline Tool - */ - tool: InlineToolName; - - /** - * Inline Tool Data if applicable - */ - data?: InlineToolData; - - /** - * Range of the fragment - */ - range: [start: number, end: number]; -} - -/** - * Serialized Inline Node value - * - * Interface used for tree nodes except TextNode which is a tree root - */ -export interface InlineTreeNodeSerialized { - /** - * Text value of the node and its subtree - */ - value: string; - - /** - * Fragments which node and its subtree contains - */ - fragments: InlineFragment[]; -} - -/** - * Root type representing serialized TextNode data - * - * Interface used for a tree root - */ -export interface TextNodeSerialized extends InlineTreeNodeSerialized { - [NODE_TYPE_HIDDEN_PROP]: BlockChildType.Text; -} diff --git a/packages/model/src/entities/inline-fragments/ParentInlineNode/__mocks__/index.ts b/packages/model/src/entities/inline-fragments/ParentInlineNode/__mocks__/index.ts index 99582fcc..082837be 100644 --- a/packages/model/src/entities/inline-fragments/ParentInlineNode/__mocks__/index.ts +++ b/packages/model/src/entities/inline-fragments/ParentInlineNode/__mocks__/index.ts @@ -1,4 +1,5 @@ -import type { InlineFragment, InlineNode } from '../../InlineNode/index.js'; +import type { InlineFragment } from '@editorjs/model-types'; +import type { InlineNode } from '../../InlineNode/index.js'; /** * We still need actual class for private and protected methods we can't mock diff --git a/packages/model/src/entities/inline-fragments/ParentInlineNode/index.ts b/packages/model/src/entities/inline-fragments/ParentInlineNode/index.ts index 8d846f42..98bd660e 100644 --- a/packages/model/src/entities/inline-fragments/ParentInlineNode/index.ts +++ b/packages/model/src/entities/inline-fragments/ParentInlineNode/index.ts @@ -1,18 +1,18 @@ import { getContext } from '../../../utils/Context.js'; -import { IndexBuilder } from '../../Index/IndexBuilder.js'; -import type { InlineFragment, InlineNode, InlineTreeNodeSerialized } from '../InlineNode/index.js'; +import { IndexBuilder } from '@editorjs/model-types'; +import type { InlineNode } from '../InlineNode/index.js'; +import type { InlineFragment, InlineTreeNodeSerialized, InlineToolData, InlineToolName } from '@editorjs/model-types'; import type { ParentNodeConstructorOptions } from '../mixins/ParentNode/index.js'; import { ParentNode } from '../mixins/ParentNode/index.js'; import type { ChildNode } from '../mixins/ChildNode/index.js'; -import type { InlineToolData, InlineToolName } from '../FormattingInlineNode/index.js'; import { TextInlineNode } from '../index.js'; -import { EventBus } from '../../../EventBus/EventBus.js'; +import { EventBus } from '@editorjs/model-types'; import { TextAddedEvent, - TextRemovedEvent, TextFormattedEvent, + TextRemovedEvent, TextUnformattedEvent -} from '../../../EventBus/events/index.js'; +} from '@editorjs/model-types'; /** * We need to extend ParentInlineNode interface with ParentNode ones to use the methods from mixins diff --git a/packages/model/src/entities/inline-fragments/TextInlineNode/index.ts b/packages/model/src/entities/inline-fragments/TextInlineNode/index.ts index 33729f6f..19735382 100644 --- a/packages/model/src/entities/inline-fragments/TextInlineNode/index.ts +++ b/packages/model/src/entities/inline-fragments/TextInlineNode/index.ts @@ -1,7 +1,7 @@ -import type { InlineToolName, InlineToolData } from '../index.js'; +import type { InlineToolName, InlineToolData, InlineTreeNodeSerialized } from '@editorjs/model-types'; import { FormattingInlineNode } from '../index.js'; import type { TextInlineNodeConstructorParameters } from './types/index.js'; -import type { InlineNode, InlineTreeNodeSerialized } from '../InlineNode/index.js'; +import type { InlineNode } from '../InlineNode/index.js'; import { ChildNode } from '../mixins/ChildNode/index.js'; export type * from './types/index.js'; diff --git a/packages/model/src/entities/inline-fragments/TextNode/TextNode.spec.ts b/packages/model/src/entities/inline-fragments/TextNode/TextNode.spec.ts index 9e2801a5..53e75c92 100644 --- a/packages/model/src/entities/inline-fragments/TextNode/TextNode.spec.ts +++ b/packages/model/src/entities/inline-fragments/TextNode/TextNode.spec.ts @@ -1,9 +1,8 @@ -import type { InlineToolName } from '../FormattingInlineNode/index.js'; -import type { InlineFragment } from '../InlineNode/index.js'; +import type { InlineToolName } from '@editorjs/model-types'; +import { BlockChildType, NODE_TYPE_HIDDEN_PROP } from '@editorjs/model-types'; +import type { InlineFragment } from '@editorjs/model-types'; import { TextNode } from './index.js'; import { ParentInlineNode } from '../ParentInlineNode/index.js'; -import { BlockChildType } from '../../BlockNode/types/index.js'; -import { NODE_TYPE_HIDDEN_PROP } from '../../BlockNode/consts.js'; jest.mock('../ParentInlineNode'); diff --git a/packages/model/src/entities/inline-fragments/TextNode/__mocks__/index.ts b/packages/model/src/entities/inline-fragments/TextNode/__mocks__/index.ts index 6587e6b4..a0ff6776 100644 --- a/packages/model/src/entities/inline-fragments/TextNode/__mocks__/index.ts +++ b/packages/model/src/entities/inline-fragments/TextNode/__mocks__/index.ts @@ -1,4 +1,4 @@ -import { EventBus } from '../../../../EventBus/EventBus.js'; +import { EventBus } from '@editorjs/model-types'; /** * Mock for TextNode class diff --git a/packages/model/src/entities/inline-fragments/TextNode/index.ts b/packages/model/src/entities/inline-fragments/TextNode/index.ts index 5218a836..4f5ae8ca 100644 --- a/packages/model/src/entities/inline-fragments/TextNode/index.ts +++ b/packages/model/src/entities/inline-fragments/TextNode/index.ts @@ -1,7 +1,7 @@ -import type { InlineFragment, InlineToolName, ParentInlineNodeConstructorOptions, TextNodeSerialized } from '../index.js'; +import type { InlineToolName, InlineFragment, TextNodeSerialized } from '@editorjs/model-types'; +import { BlockChildType, NODE_TYPE_HIDDEN_PROP } from '@editorjs/model-types'; +import type { ParentInlineNodeConstructorOptions } from '../index.js'; import { ParentInlineNode } from '../index.js'; -import { BlockChildType } from '../../BlockNode/types/index.js'; -import { NODE_TYPE_HIDDEN_PROP } from '../../BlockNode/consts.js'; interface TextNodeConstructorOptions extends ParentInlineNodeConstructorOptions { value?: string; diff --git a/packages/model/src/entities/inline-fragments/index.ts b/packages/model/src/entities/inline-fragments/index.ts index 42a93680..b1feb5ed 100644 --- a/packages/model/src/entities/inline-fragments/index.ts +++ b/packages/model/src/entities/inline-fragments/index.ts @@ -4,4 +4,4 @@ export * from './TextNode/index.js'; export * from './TextInlineNode/index.js'; export * from './mixins/ChildNode/index.js'; export * from './mixins/ParentNode/index.js'; -export type * from './InlineNode/index.js'; +export type { InlineNode } from './InlineNode/index.js'; diff --git a/packages/model/src/entities/inline-fragments/specs/ChildNode.spec.ts b/packages/model/src/entities/inline-fragments/specs/ChildNode.spec.ts index 62755346..54788720 100644 --- a/packages/model/src/entities/inline-fragments/specs/ChildNode.spec.ts +++ b/packages/model/src/entities/inline-fragments/specs/ChildNode.spec.ts @@ -48,7 +48,7 @@ describe('ChildNode mixin', () => { dummy = new Dummy({ parent: parentMock }); - expect(spy).toBeCalledWith(dummy); + expect(spy).toHaveBeenCalledWith(dummy); }); it('should add remove method to the decorated class', () => { @@ -85,7 +85,7 @@ describe('ChildNode mixin', () => { dummy.remove(); - expect(spy).toBeCalledWith(dummy); + expect(spy).toHaveBeenCalledWith(dummy); }); it('should set node\'s parent to null', () => { @@ -105,7 +105,7 @@ describe('ChildNode mixin', () => { dummy.appendTo(parentMock); - expect(spy).toBeCalledWith(dummy); + expect(spy).toHaveBeenCalledWith(dummy); }); it('should set node\'s parent on appendTo call', () => { @@ -123,7 +123,7 @@ describe('ChildNode mixin', () => { dummyWithParent.appendTo(parentMock); - expect(spy).not.toBeCalled(); + expect(spy).not.toHaveBeenCalled(); }); }); }); diff --git a/packages/model/src/entities/inline-fragments/specs/FormattingInlineNode.spec.ts b/packages/model/src/entities/inline-fragments/specs/FormattingInlineNode.spec.ts index 5df6d9b5..6e09bf58 100644 --- a/packages/model/src/entities/inline-fragments/specs/FormattingInlineNode.spec.ts +++ b/packages/model/src/entities/inline-fragments/specs/FormattingInlineNode.spec.ts @@ -1,7 +1,8 @@ +import { createInlineToolData, createInlineToolName } from '@editorjs/model-types'; +import type { InlineNode } from '../InlineNode/index.js'; import type { ChildNode } from '../index.js'; -import { TextInlineNode, createInlineToolData, createInlineToolName, FormattingInlineNode } from '../index.js'; +import { TextInlineNode, FormattingInlineNode } from '../index.js'; import type { ParentNode } from '../mixins/ParentNode/index.js'; -import type { InlineNode } from '../InlineNode/index.js'; import { ParentInlineNode } from '../ParentInlineNode/index.js'; jest.mock('../ParentInlineNode'); @@ -46,7 +47,7 @@ describe('FormattingInlineNode', () => { node.removeText(start, end); - expect(spy).toBeCalledWith(start, end); + expect(spy).toHaveBeenCalledWith(start, end); }); it('should return removed text', () => { @@ -65,7 +66,7 @@ describe('FormattingInlineNode', () => { node.removeText(); - expect(removeSpy).toBeCalled(); + expect(removeSpy).toHaveBeenCalled(); removeSpy.mockRestore(); lengthSpy.mockRestore(); @@ -81,7 +82,7 @@ describe('FormattingInlineNode', () => { node.getFragments(start, end); - expect(spy).toBeCalledWith(start, end); + expect(spy).toHaveBeenCalledWith(start, end); }); it('should return fragment with the range from 0 to the length of formatting node', () => { @@ -115,8 +116,8 @@ describe('FormattingInlineNode', () => { const index = 5; it('should throw an error if index is invalid', () => { - expect(() => node.split(-1)).toThrowError(); - expect(() => node.split(node.length + 1)).toThrowError(); + expect(() => node.split(-1)).toThrow(); + expect(() => node.split(node.length + 1)).toThrow(); }); it('should not split (return null) if index is 0', () => { @@ -148,7 +149,7 @@ describe('FormattingInlineNode', () => { node.split(index + firstChildMock.length); - expect(spy).toBeCalledWith(index); + expect(spy).toHaveBeenCalledWith(index); }); it('should insert new node to the parent', () => { @@ -156,7 +157,7 @@ describe('FormattingInlineNode', () => { const newNode = node.split(index); - expect(spy).toBeCalledWith(node, newNode); + expect(spy).toHaveBeenCalledWith(node, newNode); }); }); @@ -175,7 +176,7 @@ describe('FormattingInlineNode', () => { node.format(anotherTool, start, end, data); - expect(spy).toBeCalledWith(anotherTool, start, end, data); + expect(spy).toHaveBeenCalledWith(anotherTool, start, end, data); }); }); @@ -204,7 +205,7 @@ describe('FormattingInlineNode', () => { node.unformat(anotherTool, start, end); - expect(spy).toBeCalledWith(anotherTool, start, end); + expect(spy).toHaveBeenCalledWith(anotherTool, start, end); }); it('should not call parent unformat() method if tools are equal', () => { @@ -212,7 +213,7 @@ describe('FormattingInlineNode', () => { node.unformat(tool, start, end); - expect(spy).not.toBeCalled(); + expect(spy).not.toHaveBeenCalled(); }); it('should split node into two if unformatting applied from the start to the middle of the text', () => { @@ -294,7 +295,7 @@ describe('FormattingInlineNode', () => { node.mergeWith(nodeToMerge); - expect(spy).toBeCalledWith(child); + expect(spy).toHaveBeenCalledWith(child); }); it('should append several children of merged node to the current', () => { @@ -309,8 +310,8 @@ describe('FormattingInlineNode', () => { node.mergeWith(nodeToMerge); - expect(spy).toBeCalledWith(firstChild); - expect(spy).toBeCalledWith(secondChild); + expect(spy).toHaveBeenCalledWith(firstChild); + expect(spy).toHaveBeenCalledWith(secondChild); }); it('should remove merged node', () => { @@ -320,7 +321,7 @@ describe('FormattingInlineNode', () => { node.mergeWith(nodeToMerge); - expect(spy).toBeCalled(); + expect(spy).toHaveBeenCalled(); }); it('should throw an error if node to merge is not equal to the current', () => { diff --git a/packages/model/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts b/packages/model/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts index 61205b7b..bb66d1ed 100644 --- a/packages/model/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts +++ b/packages/model/src/entities/inline-fragments/specs/InlineTree.integration.spec.ts @@ -1,15 +1,11 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ -import type { - TextNodeSerialized -} from '../index.js'; +import { createInlineToolData, createInlineToolName, BlockChildType } from '@editorjs/model-types'; +import type { TextNodeSerialized } from '@editorjs/model-types'; import { TextInlineNode, - TextNode, - createInlineToolData, - createInlineToolName + TextNode } from '../index.js'; -import { BlockChildType } from '../../BlockNode/types/index.js'; -import { NODE_TYPE_HIDDEN_PROP } from '../../BlockNode/consts.js'; +import { NODE_TYPE_HIDDEN_PROP } from '@editorjs/model-types'; describe('Inline fragments tree integration', () => { describe('text insertion', () => { @@ -131,7 +127,7 @@ describe('Inline fragments tree integration', () => { const index = 100; expect(() => tree.removeText(index)) - .toThrowError(); + .toThrow(); }); }); diff --git a/packages/model/src/entities/inline-fragments/specs/ParentInlineNode.spec.ts b/packages/model/src/entities/inline-fragments/specs/ParentInlineNode.spec.ts index 82539ed6..ea897bcd 100644 --- a/packages/model/src/entities/inline-fragments/specs/ParentInlineNode.spec.ts +++ b/packages/model/src/entities/inline-fragments/specs/ParentInlineNode.spec.ts @@ -1,16 +1,16 @@ -import type { InlineToolName, ChildNode } from '../index.js'; +import { createInlineToolData } from '@editorjs/model-types'; +import type { InlineToolName } from '@editorjs/model-types'; +import type { InlineNode } from '../InlineNode/index.js'; +import type { ChildNode } from '../index.js'; import { TextInlineNode, FormattingInlineNode } from '../index.js'; import { ParentInlineNode } from '../ParentInlineNode/index.js'; -import type { InlineNode } from '../InlineNode/index.js'; -import { EventType } from '../../../EventBus/types/EventType.js'; +import { EventType, EventAction } from '@editorjs/model-types'; import { TextAddedEvent, TextFormattedEvent, TextRemovedEvent, TextUnformattedEvent -} from '../../../EventBus/events/index.js'; -import { EventAction } from '../../../EventBus/types/EventAction.js'; -import { createInlineToolData } from '../index.js'; +} from '@editorjs/model-types'; jest.mock('../TextInlineNode'); jest.mock('../FormattingInlineNode'); @@ -79,17 +79,17 @@ describe('ParentInlineNode', () => { const end = 5; it('should throw an error if start index is invalid', () => { - expect(() => node.getText(-1)).toThrowError(); - expect(() => node.getText(node.length + 1)).toThrowError(); + expect(() => node.getText(-1)).toThrow(); + expect(() => node.getText(node.length + 1)).toThrow(); }); it('should throw an error if end index is invalid', () => { - expect(() => node.getText(0, -1)).toThrowError(); - expect(() => node.getText(0, node.length + 1)).toThrowError(); + expect(() => node.getText(0, -1)).toThrow(); + expect(() => node.getText(0, node.length + 1)).toThrow(); }); it('should throw an error if end is less than start', () => { - expect(() => node.getText(end, start)).toThrowError(); + expect(() => node.getText(end, start)).toThrow(); }); it('should call getText() for the relevant child', () => { @@ -97,7 +97,7 @@ describe('ParentInlineNode', () => { node.getText(start, end); - expect(spy).toBeCalledWith(start, end); + expect(spy).toHaveBeenCalledWith(start, end); }); it('should adjust index by child offset', () => { @@ -107,7 +107,7 @@ describe('ParentInlineNode', () => { node.getText(offset + start, offset + end); - expect(spy).toBeCalledWith(start, end); + expect(spy).toHaveBeenCalledWith(start, end); }); it('should not call getText() for previous child if start equals its length', () => { @@ -117,7 +117,7 @@ describe('ParentInlineNode', () => { node.getText(childLength, childLength + end); - expect(spy).not.toBeCalled(); + expect(spy).not.toHaveBeenCalled(); }); it('should not call getText() for next child if end equals its start index', () => { @@ -125,7 +125,7 @@ describe('ParentInlineNode', () => { node.getText(start, childMock.length); - expect(spy).not.toBeCalled(); + expect(spy).not.toHaveBeenCalled(); }); it('should not call getText() for if start equals end', () => { @@ -133,7 +133,7 @@ describe('ParentInlineNode', () => { node.getText(start, start); - expect(spy).not.toBeCalled(); + expect(spy).not.toHaveBeenCalled(); }); it('should call getText for all relevant children', () => { @@ -143,8 +143,8 @@ describe('ParentInlineNode', () => { node.getText(start, offset + end); - expect(spy).toBeCalledWith(start, offset); - expect(anotherSpy).toBeCalledWith(0, end); + expect(spy).toHaveBeenCalledWith(start, offset); + expect(anotherSpy).toHaveBeenCalledWith(0, end); }); it('should return all text by default', () => { @@ -153,8 +153,8 @@ describe('ParentInlineNode', () => { node.getText(); - expect(spy).toBeCalledWith(0, childMock.length); - expect(anotherSpy).toBeCalledWith(0, anotherChildMock.length); + expect(spy).toHaveBeenCalledWith(0, childMock.length); + expect(anotherSpy).toHaveBeenCalledWith(0, anotherChildMock.length); }); }); @@ -163,8 +163,8 @@ describe('ParentInlineNode', () => { const index = 3; it('should throw a error if index is invalid', () => { - expect(() => node.insertText(newText, -1)).toThrowError(); - expect(() => node.insertText(newText, node.length + 1)).toThrowError(); + expect(() => node.insertText(newText, -1)).toThrow(); + expect(() => node.insertText(newText, node.length + 1)).toThrow(); }); it('should create empty text node if node is empty', () => { @@ -180,7 +180,7 @@ describe('ParentInlineNode', () => { node.insertText(newText); - expect(spy).not.toBeCalled(); + expect(spy).not.toHaveBeenCalled(); }); it('should call insertText() of the child with the passed index', () => { @@ -188,7 +188,7 @@ describe('ParentInlineNode', () => { node.insertText(newText, index); - expect(spy).toBeCalledWith(newText, index); + expect(spy).toHaveBeenCalledWith(newText, index); }); it('should adjust index by child offset', () => { @@ -198,7 +198,7 @@ describe('ParentInlineNode', () => { node.insertText(newText, index + offset); - expect(spy).toBeCalledWith(newText, index); + expect(spy).toHaveBeenCalledWith(newText, index); }); it('should append text to the last child by default', () => { @@ -206,7 +206,7 @@ describe('ParentInlineNode', () => { node.insertText(newText); - expect(spy).toBeCalledWith(newText, anotherFormattingNodeMock.length); + expect(spy).toHaveBeenCalledWith(newText, anotherFormattingNodeMock.length); }); it('should normalize subtree on insertion', () => { @@ -214,7 +214,7 @@ describe('ParentInlineNode', () => { node.insertText(newText); - expect(spy).toBeCalled(); + expect(spy).toHaveBeenCalled(); }); it('should emit TextAddedEvent with correct text as data and text range as index', () => { @@ -238,17 +238,17 @@ describe('ParentInlineNode', () => { const end = 5; it('should throw a error if start index is invalid', () => { - expect(() => node.removeText(-1)).toThrowError(); - expect(() => node.removeText(node.length + 1)).toThrowError(); + expect(() => node.removeText(-1)).toThrow(); + expect(() => node.removeText(node.length + 1)).toThrow(); }); it('should throw an error if end index is invalid', () => { - expect(() => node.removeText(0, -1)).toThrowError(); - expect(() => node.removeText(0, node.length + 1)).toThrowError(); + expect(() => node.removeText(0, -1)).toThrow(); + expect(() => node.removeText(0, node.length + 1)).toThrow(); }); it('should throw an error if end is less than start', () => { - expect(() => node.removeText(end, start)).toThrowError(); + expect(() => node.removeText(end, start)).toThrow(); }); it('should remove text from appropriate child', () => { @@ -256,7 +256,7 @@ describe('ParentInlineNode', () => { node.removeText(start, end); - expect(spy).toBeCalledWith(start, end); + expect(spy).toHaveBeenCalledWith(start, end); }); it('should adjust indices by child offset', () => { @@ -266,7 +266,7 @@ describe('ParentInlineNode', () => { node.removeText(offset + start, offset + end); - expect(spy).toBeCalledWith(start, end); + expect(spy).toHaveBeenCalledWith(start, end); }); it('should call removeText for each affected child', () => { @@ -277,8 +277,8 @@ describe('ParentInlineNode', () => { node.removeText(start, offset + end); - expect(spy).toBeCalledWith(start, offset); - expect(anotherSpy).toBeCalledWith(0, end); + expect(spy).toHaveBeenCalledWith(start, offset); + expect(anotherSpy).toHaveBeenCalledWith(0, end); }); it('should remove all text by default', () => { @@ -287,8 +287,8 @@ describe('ParentInlineNode', () => { node.removeText(); - expect(spy).toBeCalledWith(0, childMock.length); - expect(anotherSpy).toBeCalledWith(0, formattingNodeMock.length); + expect(spy).toHaveBeenCalledWith(0, childMock.length); + expect(anotherSpy).toHaveBeenCalledWith(0, formattingNodeMock.length); }); it('should normalize subtree on removal', () => { @@ -296,7 +296,7 @@ describe('ParentInlineNode', () => { node.removeText(); - expect(spy).toBeCalled(); + expect(spy).toHaveBeenCalled(); }); it('should emit TextRemovedEvent with removed text as data and range as index', () => { @@ -325,17 +325,17 @@ describe('ParentInlineNode', () => { }); it('should throw a error if start index is invalid', () => { - expect(() => node.getFragments(-1)).toThrowError(); - expect(() => node.getFragments(node.length + 1)).toThrowError(); + expect(() => node.getFragments(-1)).toThrow(); + expect(() => node.getFragments(node.length + 1)).toThrow(); }); it('should throw an error if end index is invalid', () => { - expect(() => node.getFragments(0, -1)).toThrowError(); - expect(() => node.getFragments(0, node.length + 1)).toThrowError(); + expect(() => node.getFragments(0, -1)).toThrow(); + expect(() => node.getFragments(0, node.length + 1)).toThrow(); }); it('should throw an error if end is less than start', () => { - expect(() => node.getFragments(end, start)).toThrowError(); + expect(() => node.getFragments(end, start)).toThrow(); }); it('should call getFragments for relevant children', () => { @@ -344,8 +344,8 @@ describe('ParentInlineNode', () => { node.getFragments(start, end); - expect(spyToBeCalled).toBeCalledWith(start - childMock.length, end - childMock.length); - expect(spyNotToBeCalled).not.toBeCalled(); + expect(spyToBeCalled).toHaveBeenCalledWith(start - childMock.length, end - childMock.length); + expect(spyNotToBeCalled).not.toHaveBeenCalled(); }); it('should not get fragments for text nodes', () => { @@ -419,21 +419,21 @@ describe('ParentInlineNode', () => { const tool = 'bold' as InlineToolName; it('should throw a error if start index is invalid', () => { - expect(() => node.format(tool, -1, 0)).toThrowError(); - expect(() => node.format(tool, node.length + 1, node.length)).toThrowError(); + expect(() => node.format(tool, -1, 0)).toThrow(); + expect(() => node.format(tool, node.length + 1, node.length)).toThrow(); }); it('should throw an error if end index is invalid', () => { - expect(() => node.format(tool, 0, -1)).toThrowError(); - expect(() => node.format(tool, 0, node.length + 1)).toThrowError(); + expect(() => node.format(tool, 0, -1)).toThrow(); + expect(() => node.format(tool, 0, node.length + 1)).toThrow(); }); it('should throw an error if end is less than start', () => { - expect(() => node.format(tool, end, start)).toThrowError(); + expect(() => node.format(tool, end, start)).toThrow(); }); it('should not throw an error if end equals start', () => { - expect(() => node.format(tool, start, start)).not.toThrowError(); + expect(() => node.format(tool, start, start)).not.toThrow(); }); it('should apply formatting to the relevant child', () => { @@ -441,7 +441,7 @@ describe('ParentInlineNode', () => { node.format(tool, start, end); - expect(spy).toBeCalledWith(tool, start, end, undefined); + expect(spy).toHaveBeenCalledWith(tool, start, end, undefined); }); it('should adjust index by child offset', () => { @@ -451,7 +451,7 @@ describe('ParentInlineNode', () => { node.format(tool, offset + start, offset + end); - expect(spy).toBeCalledWith(tool, start, end, undefined); + expect(spy).toHaveBeenCalledWith(tool, start, end, undefined); }); it('should format all relevant children', () => { @@ -462,8 +462,8 @@ describe('ParentInlineNode', () => { node.format(tool, start, offset + end); - expect(spy).toBeCalledWith(tool, start, offset, undefined); - expect(anotherSpy).toBeCalledWith(tool, 0, end, undefined); + expect(spy).toHaveBeenCalledWith(tool, start, offset, undefined); + expect(anotherSpy).toHaveBeenCalledWith(tool, 0, end, undefined); }); it('should normalize subtree on format', () => { @@ -471,7 +471,7 @@ describe('ParentInlineNode', () => { node.format(tool, start, end); - expect(spy).toBeCalled(); + expect(spy).toHaveBeenCalled(); }); it('should emit TextFormattedEvent with inline fragment as data and affected range as index', () => { @@ -512,7 +512,7 @@ describe('ParentInlineNode', () => { node.unformat(tool, start, end); - expect(spy).toBeCalledWith(tool, start - childMock.length, end - childMock.length); + expect(spy).toHaveBeenCalledWith(tool, start - childMock.length, end - childMock.length); }); it('should call unformat for all relevant children', () => { @@ -523,8 +523,8 @@ describe('ParentInlineNode', () => { node.unformat(tool, start, offset + end); - expect(spy).toBeCalledWith(tool, start - childMock.length, offset); - expect(anotherSpy).toBeCalledWith(tool, 0, end - childMock.length); + expect(spy).toHaveBeenCalledWith(tool, start - childMock.length, offset); + expect(anotherSpy).toHaveBeenCalledWith(tool, 0, end - childMock.length); }); it('should not unformat text nodes', () => { @@ -538,7 +538,7 @@ describe('ParentInlineNode', () => { node.unformat(tool, start, end); - expect(spy).toBeCalled(); + expect(spy).toHaveBeenCalled(); }); it('should emit TextUnformattedEvent with inline fragment as data and range as index', () => { diff --git a/packages/model/src/entities/inline-fragments/specs/ParentNode.spec.ts b/packages/model/src/entities/inline-fragments/specs/ParentNode.spec.ts index 415437c7..02d2e9ef 100644 --- a/packages/model/src/entities/inline-fragments/specs/ParentNode.spec.ts +++ b/packages/model/src/entities/inline-fragments/specs/ParentNode.spec.ts @@ -61,7 +61,7 @@ describe('ParentNode mixin', () => { children: [childMock], }); - expect(spy).toBeCalledWith(dummy); + expect(spy).toHaveBeenCalledWith(dummy); }); }); @@ -89,7 +89,7 @@ describe('ParentNode mixin', () => { children: [childMock], }); - expect(spy).toBeCalledWith(dummy); + expect(spy).toHaveBeenCalledWith(dummy); }); }); @@ -150,7 +150,7 @@ describe('ParentNode mixin', () => { dummy.insertAfter(childMock, childMockToInsert); - expect(spy).toBeCalledWith(dummy); + expect(spy).toHaveBeenCalledWith(dummy); }); it('should call appendTo for each passed child', () => { @@ -168,7 +168,7 @@ describe('ParentNode mixin', () => { dummy.insertAfter(childMock, childMockToInsert, anotherChildMockToInsert); spies.forEach((spy) => { - expect(spy).toBeCalledWith(dummy); + expect(spy).toHaveBeenCalledWith(dummy); }); }); @@ -221,7 +221,7 @@ describe('ParentNode mixin', () => { dummy.removeChild(childMock); - expect(spy).toBeCalled(); + expect(spy).toHaveBeenCalled(); }); }); diff --git a/packages/model/src/entities/inline-fragments/specs/TextInlineNode.spec.ts b/packages/model/src/entities/inline-fragments/specs/TextInlineNode.spec.ts index 553510ac..57d08686 100644 --- a/packages/model/src/entities/inline-fragments/specs/TextInlineNode.spec.ts +++ b/packages/model/src/entities/inline-fragments/specs/TextInlineNode.spec.ts @@ -1,5 +1,5 @@ import { TextInlineNode } from '../TextInlineNode/index.js'; -import type { InlineToolName } from '../FormattingInlineNode/index.js'; +import type { InlineToolName } from '@editorjs/model-types'; import { FormattingInlineNode } from '../FormattingInlineNode/index.js'; import type { ParentNode } from '../mixins/ParentNode/index.js'; import type { InlineNode } from '../InlineNode/index.js'; @@ -62,13 +62,13 @@ describe('TextInlineNode', () => { it('should throw an error if index is less than 0', () => { const f = (): void => node.insertText(text, -1); - expect(f).toThrowError(); + expect(f).toThrow(); }); it('should throw an error if index is greater than node length', () => { const f = (): void => node.insertText(text, initialText.length + 1); - expect(f).toThrowError(); + expect(f).toThrow(); }); }); @@ -93,26 +93,26 @@ describe('TextInlineNode', () => { }); it('should throw an error if start is invalid index', () => { - expect(() => node.getText(-1)).toThrowError(); - expect(() => node.getText(initialText.length + 1)).toThrowError(); + expect(() => node.getText(-1)).toThrow(); + expect(() => node.getText(initialText.length + 1)).toThrow(); }); it('should throw an error if end is invalid index', () => { - expect(() => node.getText(0, initialText.length + 1)).toThrowError(); + expect(() => node.getText(0, initialText.length + 1)).toThrow(); }); it('should throw an error if end index is greater than start index', () => { const start = 5; const end = 3; - expect(() => node.getText(start, end)).toThrowError(); + expect(() => node.getText(start, end)).toThrow(); }); it('should not throw an error if end index is equal to start index', () => { const start = 5; const end = 5; - expect(() => node.getText(start, end)).not.toThrowError(); + expect(() => node.getText(start, end)).not.toThrow(); }); }); @@ -153,7 +153,7 @@ describe('TextInlineNode', () => { node.removeText(); - expect(node.remove).toBeCalled(); + expect(node.remove).toHaveBeenCalled(); }); }); @@ -216,7 +216,7 @@ describe('TextInlineNode', () => { const fragments = node.format(name, start, end); - expect(parentMock.insertAfter).toBeCalledWith(node, ...fragments); + expect(parentMock.insertAfter).toHaveBeenCalledWith(node, ...fragments); }); }); @@ -256,7 +256,7 @@ describe('TextInlineNode', () => { it('should insert new node to the parent', () => { const newNode = node.split(index); - expect(parentMock.insertAfter).toBeCalledWith(node, newNode); + expect(parentMock.insertAfter).toHaveBeenCalledWith(node, newNode); }); }); @@ -309,7 +309,7 @@ describe('TextInlineNode', () => { }); it('should throw an error if node to merge is not TextInlineNode', () => { - expect(() => node.mergeWith({} as InlineNode)).toThrowError(); + expect(() => node.mergeWith({} as InlineNode)).toThrow(); }); }); }); diff --git a/packages/model/src/entities/inline-fragments/specs/TextNode.spec.ts b/packages/model/src/entities/inline-fragments/specs/TextNode.spec.ts index ea9ce259..19731d28 100644 --- a/packages/model/src/entities/inline-fragments/specs/TextNode.spec.ts +++ b/packages/model/src/entities/inline-fragments/specs/TextNode.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ import { TextNode } from '../TextNode/index.js'; -import type { InlineToolData, InlineToolName } from '../FormattingInlineNode/index.js'; +import type { InlineToolData, InlineToolName } from '@editorjs/model-types'; jest.mock('../mixins/ChildNode'); jest.mock('../mixins/ParentNode'); diff --git a/packages/model/src/mocks/data.ts b/packages/model/src/mocks/data.ts index cc3d7a42..5d894e00 100644 --- a/packages/model/src/mocks/data.ts +++ b/packages/model/src/mocks/data.ts @@ -1,7 +1,7 @@ // Stryker disable all /* eslint-disable @typescript-eslint/no-magic-numbers */ import type { EditorDocumentSerialized } from '../entities/EditorDocument/types/index.js'; -import { createBlockId } from '../entities/BlockNode/types/BlockId.js'; +import { createBlockId } from '@editorjs/model-types'; export const data: EditorDocumentSerialized = { identifier: 'document', diff --git a/packages/model/src/tools/ToolsRegistry.spec.ts b/packages/model/src/tools/ToolsRegistry.spec.ts index 7c949511..808748b7 100644 --- a/packages/model/src/tools/ToolsRegistry.spec.ts +++ b/packages/model/src/tools/ToolsRegistry.spec.ts @@ -10,7 +10,7 @@ describe('ToolsRegistry', () => { registry.get(toolName); - expect(spy).toBeCalledWith(toolName); + expect(spy).toHaveBeenCalledWith(toolName); }); }); }); diff --git a/packages/model/src/tools/ToolsRegistry.ts b/packages/model/src/tools/ToolsRegistry.ts index 763cd646..38dc9be5 100644 --- a/packages/model/src/tools/ToolsRegistry.ts +++ b/packages/model/src/tools/ToolsRegistry.ts @@ -1,5 +1,5 @@ /* eslint-disable jsdoc/require-jsdoc */ -import type { BlockToolName, InlineToolName } from '../entities/index.js'; +import type { BlockToolName, InlineToolName } from '@editorjs/model-types'; import type { BlockToolConstructable, InlineToolConstructable } from './types/index.js'; /** diff --git a/packages/model/src/utils/index.ts b/packages/model/src/utils/index.ts index 9169aa12..14dafaf1 100644 --- a/packages/model/src/utils/index.ts +++ b/packages/model/src/utils/index.ts @@ -1,3 +1,3 @@ -export * from './Nominal.js'; -export * as keypath from './keypath.js'; +export type { Nominal } from '@editorjs/model-types'; +export { create } from '@editorjs/model-types'; export * from './textUtils.js'; diff --git a/packages/model/src/utils/textUtils.spec.ts b/packages/model/src/utils/textUtils.spec.ts index d889f739..8d9d9d33 100644 --- a/packages/model/src/utils/textUtils.spec.ts +++ b/packages/model/src/utils/textUtils.spec.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-magic-numbers, @typescript-eslint/no-explicit-any */ import { sliceFragments, mergeTextNodes } from './textUtils.js'; -import type { InlineFragment, InlineTreeNodeSerialized, TextNodeSerialized } from '../entities/inline-fragments/InlineNode/index.js'; -import type { InlineToolName } from '../entities/inline-fragments/index.js'; -import { BlockChildType, NODE_TYPE_HIDDEN_PROP } from '../entities/BlockNode/index.js'; +import type { InlineFragment, InlineTreeNodeSerialized, TextNodeSerialized } from '@editorjs/model-types'; +import type { InlineToolName } from '@editorjs/model-types'; +import { BlockChildType, NODE_TYPE_HIDDEN_PROP } from '@editorjs/model-types'; /** * Helper to create a TextNodeSerialized with the required hidden-type prop. diff --git a/packages/model/src/utils/textUtils.ts b/packages/model/src/utils/textUtils.ts index d15da4d0..a519acf0 100644 --- a/packages/model/src/utils/textUtils.ts +++ b/packages/model/src/utils/textUtils.ts @@ -1,4 +1,4 @@ -import type { InlineFragment, InlineTreeNodeSerialized, TextNodeSerialized } from '../entities/inline-fragments/InlineNode/index.js'; +import type { InlineFragment, InlineTreeNodeSerialized, TextNodeSerialized } from '@editorjs/model-types'; /** * Returns the fragments that fall after a given character offset, diff --git a/packages/model/tsconfig.json b/packages/model/tsconfig.json index f9b22ef7..d8f1841b 100644 --- a/packages/model/tsconfig.json +++ b/packages/model/tsconfig.json @@ -16,5 +16,10 @@ "exclude": [ "node_modules/**/*", "dist/**/*" + ], + "references": [ + { + "path": "../model-types/tsconfig.build.json" + } ] } diff --git a/packages/ot-server/package.json b/packages/ot-server/package.json index ec00d36f..ec244bc5 100644 --- a/packages/ot-server/package.json +++ b/packages/ot-server/package.json @@ -19,17 +19,18 @@ "dependencies": { "@editorjs/collaboration-manager": "workspace:^", "@editorjs/model": "workspace:^", + "@editorjs/sdk": "workspace:^", "@hawk.so/nodejs": "^3.1.5", "dotenv": "^16.4.7", "ws": "^8.18.1" }, "devDependencies": { "@types/eslint": "^9.6.1", - "@types/jest": "^29.5.14", + "@types/jest": "^30.0.0", "@types/ws": "^8.18.1", "eslint": "^9.24.0", "eslint-config-codex": "^2.0.3", - "jest": "^29.7.0", + "jest": "^30.4.2", "ts-node": "^10.9.2", "typescript": "^5.8.3" } diff --git a/packages/ot-server/src/DocumentManager.spec.ts b/packages/ot-server/src/DocumentManager.spec.ts index 15e9f409..a5d47c7e 100644 --- a/packages/ot-server/src/DocumentManager.spec.ts +++ b/packages/ot-server/src/DocumentManager.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ import { Operation, OperationType, type OperationTypeToData } from '@editorjs/collaboration-manager'; -import { IndexBuilder } from '@editorjs/model'; +import { IndexBuilder } from '@editorjs/sdk'; import { DocumentManager } from './DocumentManager.js'; // eslint-disable-next-line jsdoc/require-param diff --git a/packages/playground/package.json b/packages/playground/package.json index 23c08ca1..ca0cbcb8 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -20,6 +20,7 @@ "@editorjs/core": "workspace:^", "@editorjs/dom-adapters": "workspace:^", "@editorjs/model": "workspace:^", + "@editorjs/sdk": "workspace:^", "@editorjs/ui": "workspace:*", "vue": "^3.3.4" }, diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index 87e6ed84..84a7e58a 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -54,10 +54,10 @@ onMounted(() => { ], }, - onModelUpdate: (m: EditorJSModel) => { - model.value = m; - serialized.value = m.serialized; - editorDocument.value = m.devModeGetDocument(); + onModelUpdate: (m: unknown) => { + model.value = m as EditorJSModel; + serialized.value = (m as EditorJSModel).serialized; + editorDocument.value = (m as EditorJSModel).devModeGetDocument(); }, }); diff --git a/packages/playground/src/components/CaretIndex.vue b/packages/playground/src/components/CaretIndex.vue index 70ca24f0..90b6ea6e 100644 --- a/packages/playground/src/components/CaretIndex.vue +++ b/packages/playground/src/components/CaretIndex.vue @@ -1,5 +1,6 @@