@ecsplico/editor 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/adapters/comments/record.d.ts +15 -0
  3. package/dist/adapters/comments/server.d.ts +2 -1
  4. package/dist/adapters/comments/server.js +2 -1
  5. package/dist/adapters/comments/server.js.map +1 -1
  6. package/dist/adapters/nitro.d.ts +4 -0
  7. package/dist/adapters/nitro.js +2 -1
  8. package/dist/adapters/nitro.js.map +1 -1
  9. package/dist/code/CodeSurface.vue.d.ts +2 -2
  10. package/dist/core/command-types.d.ts +7 -0
  11. package/dist/core/commands.d.ts +2 -2
  12. package/dist/core/commands.js.map +1 -1
  13. package/dist/core/feature.d.ts +8 -2
  14. package/dist/core/feature.js.map +1 -1
  15. package/dist/core/index.d.ts +3 -2
  16. package/dist/core/index.js +2 -1
  17. package/dist/core/surface-session.js +2 -0
  18. package/dist/core/surface-session.js.map +1 -1
  19. package/dist/core/transaction-guards.d.ts +21 -0
  20. package/dist/core/transaction-guards.js +49 -0
  21. package/dist/core/transaction-guards.js.map +1 -0
  22. package/dist/document/DocumentSurface.vue.d.ts +2 -2
  23. package/dist/features/ai/protocol/chat.js +2 -1
  24. package/dist/features/ai/protocol/chat.js.map +1 -1
  25. package/dist/features/composites/ai-brief.js +8 -0
  26. package/dist/features/composites/ai-brief.js.map +1 -1
  27. package/dist/features/composites/components/CompositeChrome.js +4 -4
  28. package/dist/features/composites/components/CompositeChrome.js.map +1 -1
  29. package/dist/features/composites/components/CompositeChrome.vue.d.ts +1 -1
  30. package/dist/features/composites/components/CompositeFieldEditor.js +32 -6
  31. package/dist/features/composites/components/CompositeFieldEditor.js.map +1 -1
  32. package/dist/features/composites/components/CompositeInspector.js +3 -2
  33. package/dist/features/composites/components/CompositeInspector.js.map +1 -1
  34. package/dist/features/composites/inspector-model.js +2 -2
  35. package/dist/features/composites/inspector-model.js.map +1 -1
  36. package/dist/features/composites/kinds.d.ts +10 -3
  37. package/dist/features/composites/kinds.js.map +1 -1
  38. package/dist/features/composites/meta-display.js +5 -1
  39. package/dist/features/composites/meta-display.js.map +1 -1
  40. package/dist/features/composites/react/CompositeChrome.js +3 -3
  41. package/dist/features/composites/react/CompositeChrome.js.map +1 -1
  42. package/dist/features/composites/react/CompositeFieldEditor.js +19 -0
  43. package/dist/features/composites/react/CompositeFieldEditor.js.map +1 -1
  44. package/dist/features/composites/react/CompositeInspector.js +1 -1
  45. package/dist/features/composites/react/CompositeInspector.js.map +1 -1
  46. package/dist/features/composites/rows.d.ts +4 -1
  47. package/dist/features/composites/rows.js +9 -1
  48. package/dist/features/composites/rows.js.map +1 -1
  49. package/dist/features/composites/table-model.js +2 -2
  50. package/dist/features/composites/table-model.js.map +1 -1
  51. package/dist/features/media/index.js +1 -1
  52. package/dist/features/media/react/AudioRecorderModal.js +1 -1
  53. package/dist/features/media/react.js +1 -1
  54. package/dist/markdown/MarkdownSurface.vue.d.ts +6 -6
  55. package/dist/react/compose.js +1 -1
  56. package/dist/server/h3.d.ts +8 -0
  57. package/dist/server/handlers/ai.d.ts +2 -1
  58. package/dist/server/handlers/ai.js +2 -1
  59. package/dist/server/handlers/ai.js.map +1 -1
  60. package/dist/server/routes.d.ts +22 -0
  61. package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
  62. package/dist/vue/SimpleEditor.vue.d.ts +4 -4
  63. package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
  64. package/dist/vue/compose.js +1 -1
  65. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.1.0
4
+
5
+ ### Added
6
+
7
+ - Composites: field type `multiselect` (`CompositeMetaField`; `options` required, value stored as `string[]`; the key is removed when none is chosen). It is rendered in the details popover and inspector (Vue `InputMenu`, React checkbox list), in table cells and sort keys, in the AI brief and in the chat block model.
8
+ - Composites: `CompositeKind.chrome?: { tags?: boolean }`; `chrome: { tags: false }` hides that kind's tag chips, `+` chip and inspector tags field (`CompositeRow.tagsVisible`).
9
+ - `EditorFeature.transactionGuards(ctx)` and the `TransactionGuard` type: features can refuse a whole document-changing transaction by returning a notice string. The first refusal wins and the string is shown through `notify`. The `TransactionGuards` extension and `transactionGuardExtensions(preset, ctx)` are exported from `core`.
10
+ - `EditorServerOptions.routes` (`installEditorServer`): a function that filters or reorders the route table before it is mounted, so a host that serves some paths itself (for example `/api/editor/assets/*`) can drop them. The `RouteDef` type is exported from `@ecsplico/editor/server/nitro`.
11
+ - `@ecsplico/editor/comments/server` exports the pure discussion operations `createThread`, `addComment`, `editComment`, `removeComment`, `setThreadStatus`, `updateAnchor`, `removeThread` and `emptyDiscussion`, so a host `CommentStore` over its own database applies them to a stored `Discussion` instead of reimplementing the rules (guide „Storage adapters“, section „Your own store“).
12
+
3
13
  ## 3.0.0
4
14
 
5
15
  ### Added
@@ -0,0 +1,15 @@
1
+ import { Annotation, Comment, CommentThread, Discussion, NewThread, ThreadStatus } from "./types.js";
2
+ //#region src/adapters/comments/record.d.ts
3
+ export declare const emptyDiscussion: () => Discussion;
4
+ export declare function createThread(record: Discussion, input: NewThread): {
5
+ annotation: Annotation;
6
+ thread: CommentThread;
7
+ };
8
+ export declare function addComment(record: Discussion, threadId: string, comment: Comment): CommentThread;
9
+ export declare function editComment(record: Discussion, threadId: string, commentId: string, body: string, editedAt: string): CommentThread;
10
+ export declare function removeComment(record: Discussion, threadId: string, commentId: string): CommentThread;
11
+ export declare function setThreadStatus(record: Discussion, threadId: string, status: ThreadStatus): CommentThread;
12
+ export declare function updateAnchor(record: Discussion, annotation: Annotation): Annotation;
13
+ export declare function removeThread(record: Discussion, threadId: string): void;
14
+ //#endregion
15
+ //# sourceMappingURL=record.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import { Annotation, Comment, CommentAction, CommentStore, CommentThread, CreateThreadInput, Discussion, NewThread, ThreadStatus } from "./types.js";
2
2
  import { CommentStoreError } from "./errors.js";
3
+ import { addComment, createThread, editComment, emptyDiscussion, removeComment, removeThread, setThreadStatus, updateAnchor } from "./record.js";
3
4
  //#region src/adapters/comments/server.d.ts
4
5
  export interface CommentsRequest {
5
6
  method: string;
@@ -19,5 +20,5 @@ export interface CommentsResponse {
19
20
  */
20
21
  export declare function handleCommentsRequest(store: CommentStore, request: CommentsRequest): Promise<CommentsResponse>;
21
22
  //#endregion
22
- export { type Annotation, type Comment, type CommentAction, type CommentStore, CommentStoreError, type CommentThread, type CreateThreadInput, type Discussion, type NewThread, type ThreadStatus };
23
+ export { type Annotation, type Comment, type CommentAction, type CommentStore, CommentStoreError, type CommentThread, type CreateThreadInput, type Discussion, type NewThread, type ThreadStatus, addComment, createThread, editComment, emptyDiscussion, removeComment, removeThread, setThreadStatus, updateAnchor };
23
24
  //# sourceMappingURL=server.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import { isPortableAnchor } from "../anchors.js";
2
2
  import { CommentStoreError, invalid } from "./errors.js";
3
+ import { addComment, createThread, editComment, emptyDiscussion, removeComment, removeThread, setThreadStatus, updateAnchor } from "./record.js";
3
4
  //#region src/adapters/comments/server.ts
4
5
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
5
6
  const isText = (value) => typeof value === "string";
@@ -97,6 +98,6 @@ async function handleCommentsRequest(store, request) {
97
98
  }
98
99
  }
99
100
  //#endregion
100
- export { CommentStoreError, handleCommentsRequest };
101
+ export { CommentStoreError, addComment, createThread, editComment, emptyDiscussion, handleCommentsRequest, removeComment, removeThread, setThreadStatus, updateAnchor };
101
102
 
102
103
  //# sourceMappingURL=server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":[],"sources":["../../../src/adapters/comments/server.ts"],"sourcesContent":["import { isPortableAnchor } from '../anchors'\nimport { CommentStoreError, invalid } from './errors'\nimport type { Annotation, Comment, CommentStore, CreateThreadInput, ThreadStatus } from './types'\n\n// Framework-free REST handler of a document's comments endpoint: the host routes `GET`/`POST` of\n// `<base>/<documentId>` here after its own authentication and authorisation (the handler trusts the caller, including\n// the `authorId` of new comments), and answers with `status` and `body`. Nitro, Express or a test's fetch stub all fit.\n\nexport type { Annotation, Comment, CommentAction, CommentStore, CommentThread, CreateThreadInput, Discussion, NewThread, ThreadStatus } from './types'\nexport { CommentStoreError } from './errors'\n\nexport interface CommentsRequest {\n method: string\n documentId: string\n /** The parsed JSON body of a `POST`. */\n body?: unknown\n}\n\nexport interface CommentsResponse {\n status: number\n /** JSON; `{ error }` for a refused request. */\n body: unknown\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)\nconst isText = (value: unknown): value is string => typeof value === 'string'\nconst isId = (value: unknown): value is string => isText(value) && value !== ''\n\nfunction commentOf(value: unknown): Comment {\n if (!isRecord(value) || !isId(value.id) || !isText(value.body) || !isId(value.authorId) || !isId(value.createdAt)) throw invalid('Ein Kommentar braucht id, body, authorId und createdAt')\n for (const key of ['authorName', 'authorColor', 'editedAt']) {\n if (value[key] !== undefined && !isText(value[key])) throw invalid(`Kommentar-Feld „${key}“ muss Text sein`)\n }\n const { id, body, authorId, createdAt } = value\n const { authorName, authorColor, editedAt } = value as Record<string, string | undefined>\n return { id, body, authorId, createdAt, ...(authorName !== undefined && { authorName }), ...(authorColor !== undefined && { authorColor }), ...(editedAt !== undefined && { editedAt }) }\n}\n\nfunction annotationOf(value: unknown): Annotation {\n if (!isRecord(value) || !isId(value.id) || !isId(value.threadId) || !isPortableAnchor(value.anchor)) throw invalid('Eine Annotation braucht id, threadId und einen gültigen anchor')\n return { id: value.id, threadId: value.threadId, anchor: value.anchor }\n}\n\nfunction threadInputOf(value: unknown): CreateThreadInput {\n if (!isRecord(value) || !isId(value.id) || !isId(value.annotationId)) throw invalid('Ein Thread braucht id, annotationId und initialComment')\n return { id: value.id, annotationId: value.annotationId, initialComment: commentOf(value.initialComment) }\n}\n\nfunction idOf(value: Record<string, unknown>, key: string): string {\n const id = value[key]\n if (!isId(id)) throw invalid(`„${key}“ muss ein nicht leerer String sein`)\n return id\n}\n\nfunction statusOf(value: unknown): ThreadStatus {\n if (value !== 'open' && value !== 'resolved') throw invalid('„status“ muss „open“ oder „resolved“ sein')\n return value\n}\n\nasync function perform(store: CommentStore, documentId: string, body: unknown): Promise<unknown> {\n if (!isRecord(body)) throw invalid('Der Request-Body muss ein JSON-Objekt mit „action“ sein')\n switch (body.action) {\n case 'create':\n return store.createThread(documentId, { annotation: annotationOf(body.annotation), thread: threadInputOf(body.thread) })\n case 'reply':\n return { thread: await store.addComment(documentId, idOf(body, 'threadId'), commentOf(body.comment)) }\n case 'status':\n return { thread: await store.setThreadStatus(documentId, idOf(body, 'threadId'), statusOf(body.status)) }\n case 'update-annotation':\n return { annotation: await store.updateAnchor(documentId, annotationOf(body.annotation)) }\n case 'remove':\n await store.removeThread(documentId, idOf(body, 'threadId'))\n return { ok: true }\n case 'edit':\n if (!isText(body.body)) throw invalid('„body“ muss Text sein')\n return { thread: await store.editComment(documentId, idOf(body, 'threadId'), idOf(body, 'commentId'), body.body, isId(body.editedAt) ? body.editedAt : new Date().toISOString()) }\n case 'remove-comment':\n return { thread: await store.removeComment(documentId, idOf(body, 'threadId'), idOf(body, 'commentId')) }\n default:\n throw invalid('Unbekannte Kommentar-Aktion')\n }\n}\n\n/**\n * `GET` → `200` with the document's `Discussion`; `POST` with a `CommentAction` → `200` with the changed `thread`/`annotation`\n * (`create`: both; `remove`: `{ ok: true }`). Refused requests answer `404` (unknown ids), `409` (duplicate ids), `400`\n * (malformed input) or `405` with `{ error }`; anything else is a server fault and is thrown.\n */\nexport async function handleCommentsRequest(store: CommentStore, request: CommentsRequest): Promise<CommentsResponse> {\n const method = request.method.toUpperCase()\n try {\n if (method === 'GET') return { status: 200, body: await store.getDiscussion(request.documentId) }\n if (method === 'POST') return { status: 200, body: await perform(store, request.documentId, request.body) }\n return { status: 405, body: { error: `Methode ${request.method} ist nicht erlaubt` } } // i18n-ignore: HTTP error body of the comments route (server messages stay German, like src/server)\n }\n catch (error) {\n if (error instanceof CommentStoreError) return { status: error.status, body: { error: error.message } }\n throw error\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":[],"sources":["../../../src/adapters/comments/server.ts"],"sourcesContent":["import { isPortableAnchor } from '../anchors'\nimport { CommentStoreError, invalid } from './errors'\nimport type { Annotation, Comment, CommentStore, CreateThreadInput, ThreadStatus } from './types'\n\n// Framework-free REST handler of a document's comments endpoint: the host routes `GET`/`POST` of\n// `<base>/<documentId>` here after its own authentication and authorisation (the handler trusts the caller, including\n// the `authorId` of new comments), and answers with `status` and `body`. Nitro, Express or a test's fetch stub all fit.\n\nexport type { Annotation, Comment, CommentAction, CommentStore, CommentThread, CreateThreadInput, Discussion, NewThread, ThreadStatus } from './types'\nexport { CommentStoreError } from './errors'\n// The pure operations of a stored `Discussion` (the JSON store is built from them): a host `CommentStore` over its own database applies them to the record it loaded and writes the record back. They change the record in place and throw `CommentStoreError` for unknown ids, duplicates and malformed input.\nexport { addComment, createThread, editComment, emptyDiscussion, removeComment, removeThread, setThreadStatus, updateAnchor } from './record'\n\nexport interface CommentsRequest {\n method: string\n documentId: string\n /** The parsed JSON body of a `POST`. */\n body?: unknown\n}\n\nexport interface CommentsResponse {\n status: number\n /** JSON; `{ error }` for a refused request. */\n body: unknown\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)\nconst isText = (value: unknown): value is string => typeof value === 'string'\nconst isId = (value: unknown): value is string => isText(value) && value !== ''\n\nfunction commentOf(value: unknown): Comment {\n if (!isRecord(value) || !isId(value.id) || !isText(value.body) || !isId(value.authorId) || !isId(value.createdAt)) throw invalid('Ein Kommentar braucht id, body, authorId und createdAt')\n for (const key of ['authorName', 'authorColor', 'editedAt']) {\n if (value[key] !== undefined && !isText(value[key])) throw invalid(`Kommentar-Feld „${key}“ muss Text sein`)\n }\n const { id, body, authorId, createdAt } = value\n const { authorName, authorColor, editedAt } = value as Record<string, string | undefined>\n return { id, body, authorId, createdAt, ...(authorName !== undefined && { authorName }), ...(authorColor !== undefined && { authorColor }), ...(editedAt !== undefined && { editedAt }) }\n}\n\nfunction annotationOf(value: unknown): Annotation {\n if (!isRecord(value) || !isId(value.id) || !isId(value.threadId) || !isPortableAnchor(value.anchor)) throw invalid('Eine Annotation braucht id, threadId und einen gültigen anchor')\n return { id: value.id, threadId: value.threadId, anchor: value.anchor }\n}\n\nfunction threadInputOf(value: unknown): CreateThreadInput {\n if (!isRecord(value) || !isId(value.id) || !isId(value.annotationId)) throw invalid('Ein Thread braucht id, annotationId und initialComment')\n return { id: value.id, annotationId: value.annotationId, initialComment: commentOf(value.initialComment) }\n}\n\nfunction idOf(value: Record<string, unknown>, key: string): string {\n const id = value[key]\n if (!isId(id)) throw invalid(`„${key}“ muss ein nicht leerer String sein`)\n return id\n}\n\nfunction statusOf(value: unknown): ThreadStatus {\n if (value !== 'open' && value !== 'resolved') throw invalid('„status“ muss „open“ oder „resolved“ sein')\n return value\n}\n\nasync function perform(store: CommentStore, documentId: string, body: unknown): Promise<unknown> {\n if (!isRecord(body)) throw invalid('Der Request-Body muss ein JSON-Objekt mit „action“ sein')\n switch (body.action) {\n case 'create':\n return store.createThread(documentId, { annotation: annotationOf(body.annotation), thread: threadInputOf(body.thread) })\n case 'reply':\n return { thread: await store.addComment(documentId, idOf(body, 'threadId'), commentOf(body.comment)) }\n case 'status':\n return { thread: await store.setThreadStatus(documentId, idOf(body, 'threadId'), statusOf(body.status)) }\n case 'update-annotation':\n return { annotation: await store.updateAnchor(documentId, annotationOf(body.annotation)) }\n case 'remove':\n await store.removeThread(documentId, idOf(body, 'threadId'))\n return { ok: true }\n case 'edit':\n if (!isText(body.body)) throw invalid('„body“ muss Text sein')\n return { thread: await store.editComment(documentId, idOf(body, 'threadId'), idOf(body, 'commentId'), body.body, isId(body.editedAt) ? body.editedAt : new Date().toISOString()) }\n case 'remove-comment':\n return { thread: await store.removeComment(documentId, idOf(body, 'threadId'), idOf(body, 'commentId')) }\n default:\n throw invalid('Unbekannte Kommentar-Aktion')\n }\n}\n\n/**\n * `GET` → `200` with the document's `Discussion`; `POST` with a `CommentAction` → `200` with the changed `thread`/`annotation`\n * (`create`: both; `remove`: `{ ok: true }`). Refused requests answer `404` (unknown ids), `409` (duplicate ids), `400`\n * (malformed input) or `405` with `{ error }`; anything else is a server fault and is thrown.\n */\nexport async function handleCommentsRequest(store: CommentStore, request: CommentsRequest): Promise<CommentsResponse> {\n const method = request.method.toUpperCase()\n try {\n if (method === 'GET') return { status: 200, body: await store.getDiscussion(request.documentId) }\n if (method === 'POST') return { status: 200, body: await perform(store, request.documentId, request.body) }\n return { status: 405, body: { error: `Methode ${request.method} ist nicht erlaubt` } } // i18n-ignore: HTTP error body of the comments route (server messages stay German, like src/server)\n }\n catch (error) {\n if (error instanceof CommentStoreError) return { status: error.status, body: { error: error.message } }\n throw error\n }\n}\n"],"mappings":";;;;AA0BA,MAAM,YAAY,UAAqD,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC1I,MAAM,UAAU,UAAoC,OAAO,UAAU;AACrE,MAAM,QAAQ,UAAoC,OAAO,KAAK,KAAK,UAAU;AAE7E,SAAS,UAAU,OAAyB;CAC1C,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,OAAO,MAAM,IAAI,KAAK,CAAC,KAAK,MAAM,QAAQ,KAAK,CAAC,KAAK,MAAM,SAAS,GAAG,MAAM,QAAQ,wDAAwD;CACzL,KAAK,MAAM,OAAO;EAAC;EAAc;EAAe;CAAU,GACxD,IAAI,MAAM,SAAS,KAAA,KAAa,CAAC,OAAO,MAAM,IAAI,GAAG,MAAM,QAAQ,mBAAmB,IAAI,iBAAiB;CAE7G,MAAM,EAAE,IAAI,MAAM,UAAU,cAAc;CAC1C,MAAM,EAAE,YAAY,aAAa,aAAa;CAC9C,OAAO;EAAE;EAAI;EAAM;EAAU;EAAW,GAAI,eAAe,KAAA,KAAa,EAAE,WAAW;EAAI,GAAI,gBAAgB,KAAA,KAAa,EAAE,YAAY;EAAI,GAAI,aAAa,KAAA,KAAa,EAAE,SAAS;CAAG;AAC1L;AAEA,SAAS,aAAa,OAA4B;CAChD,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,MAAM,QAAQ,KAAK,CAAC,iBAAiB,MAAM,MAAM,GAAG,MAAM,QAAQ,gEAAgE;CACnL,OAAO;EAAE,IAAI,MAAM;EAAI,UAAU,MAAM;EAAU,QAAQ,MAAM;CAAO;AACxE;AAEA,SAAS,cAAc,OAAmC;CACxD,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,MAAM,YAAY,GAAG,MAAM,QAAQ,wDAAwD;CAC5I,OAAO;EAAE,IAAI,MAAM;EAAI,cAAc,MAAM;EAAc,gBAAgB,UAAU,MAAM,cAAc;CAAE;AAC3G;AAEA,SAAS,KAAK,OAAgC,KAAqB;CACjE,MAAM,KAAK,MAAM;CACjB,IAAI,CAAC,KAAK,EAAE,GAAG,MAAM,QAAQ,IAAI,IAAI,oCAAoC;CACzE,OAAO;AACT;AAEA,SAAS,SAAS,OAA8B;CAC9C,IAAI,UAAU,UAAU,UAAU,YAAY,MAAM,QAAQ,2CAA2C;CACvG,OAAO;AACT;AAEA,eAAe,QAAQ,OAAqB,YAAoB,MAAiC;CAC/F,IAAI,CAAC,SAAS,IAAI,GAAG,MAAM,QAAQ,yDAAyD;CAC5F,QAAQ,KAAK,QAAb;EACE,KAAK,UACH,OAAO,MAAM,aAAa,YAAY;GAAE,YAAY,aAAa,KAAK,UAAU;GAAG,QAAQ,cAAc,KAAK,MAAM;EAAE,CAAC;EACzH,KAAK,SACH,OAAO,EAAE,QAAQ,MAAM,MAAM,WAAW,YAAY,KAAK,MAAM,UAAU,GAAG,UAAU,KAAK,OAAO,CAAC,EAAE;EACvG,KAAK,UACH,OAAO,EAAE,QAAQ,MAAM,MAAM,gBAAgB,YAAY,KAAK,MAAM,UAAU,GAAG,SAAS,KAAK,MAAM,CAAC,EAAE;EAC1G,KAAK,qBACH,OAAO,EAAE,YAAY,MAAM,MAAM,aAAa,YAAY,aAAa,KAAK,UAAU,CAAC,EAAE;EAC3F,KAAK;GACH,MAAM,MAAM,aAAa,YAAY,KAAK,MAAM,UAAU,CAAC;GAC3D,OAAO,EAAE,IAAI,KAAK;EACpB,KAAK;GACH,IAAI,CAAC,OAAO,KAAK,IAAI,GAAG,MAAM,QAAQ,uBAAuB;GAC7D,OAAO,EAAE,QAAQ,MAAM,MAAM,YAAY,YAAY,KAAK,MAAM,UAAU,GAAG,KAAK,MAAM,WAAW,GAAG,KAAK,MAAM,KAAK,KAAK,QAAQ,IAAI,KAAK,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY,CAAC,EAAE;EACnL,KAAK,kBACH,OAAO,EAAE,QAAQ,MAAM,MAAM,cAAc,YAAY,KAAK,MAAM,UAAU,GAAG,KAAK,MAAM,WAAW,CAAC,EAAE;EAC1G,SACE,MAAM,QAAQ,6BAA6B;CAC/C;AACF;;;;;;AAOA,eAAsB,sBAAsB,OAAqB,SAAqD;CACpH,MAAM,SAAS,QAAQ,OAAO,YAAY;CAC1C,IAAI;EACF,IAAI,WAAW,OAAO,OAAO;GAAE,QAAQ;GAAK,MAAM,MAAM,MAAM,cAAc,QAAQ,UAAU;EAAE;EAChG,IAAI,WAAW,QAAQ,OAAO;GAAE,QAAQ;GAAK,MAAM,MAAM,QAAQ,OAAO,QAAQ,YAAY,QAAQ,IAAI;EAAE;EAC1G,OAAO;GAAE,QAAQ;GAAK,MAAM,EAAE,OAAO,WAAW,QAAQ,OAAO,oBAAoB;EAAE;CACvF,SACO,OAAO;EACZ,IAAI,iBAAiB,mBAAmB,OAAO;GAAE,QAAQ,MAAM;GAAQ,MAAM,EAAE,OAAO,MAAM,QAAQ;EAAE;EACtG,MAAM;CACR;AACF"}
@@ -3,6 +3,7 @@ import { EditorPorts } from "../server/ports.js";
3
3
  import { CollabServer, CollabServerOptions } from "../server/collab.js";
4
4
  import { RateLimiterOptions } from "../server/rate-limit.js";
5
5
  import "../server/index.js";
6
+ import { RouteDef } from "../server/routes.js";
6
7
  import { AnyExtension } from "@tiptap/core";
7
8
  import { Router } from "h3";
8
9
  //#region src/adapters/nitro.d.ts
@@ -34,6 +35,8 @@ export interface EditorServerOptions {
34
35
  ai?: Partial<AiModelsConfig>;
35
36
  /** Per-user limit of the AI endpoints; default: that of `defineEditorConfig` (30 requests per minute). */
36
37
  rateLimit?: RateLimiterOptions;
38
+ /** Filters or reorders the routes before they are mounted (a host that serves `/api/editor/assets/*` itself drops them); default: all. Receives what `serverRoutesOf(ports, { ai })` returns, so a route of an absent port, or an AI route without the `ai` option, is not in it. */
39
+ routes?: (routes: readonly RouteDef[]) => readonly RouteDef[];
37
40
  /** The collaboration schema (see `CollabServerOptions.schema`); default `advancedSchemaExtensions()`, host nodes: `() => [...advancedSchemaExtensions(), Callout]`. */
38
41
  schema?: () => AnyExtension[];
39
42
  }
@@ -61,4 +64,5 @@ export interface EditorServer {
61
64
  */
62
65
  export declare function installEditorServer(nitroApp: NitroHost, options: EditorServerOptions): EditorServer;
63
66
  //#endregion
67
+ export type { RouteDef };
64
68
  //# sourceMappingURL=nitro.d.ts.map
@@ -77,7 +77,8 @@ function installEditorServer(nitroApp, options) {
77
77
  ai: models,
78
78
  limiter
79
79
  };
80
- for (const route of serverRoutesOf(ports, { ai: aiContext !== void 0 })) {
80
+ const mounted = serverRoutesOf(ports, { ai: aiContext !== void 0 });
81
+ for (const route of options.routes ? options.routes(mounted) : mounted) {
81
82
  const handler = route.port === "ai" ? routeOf(aiContext, route.handler) : routeOf(context, route.handler);
82
83
  nitroApp.router.add(route.path, handler, route.method);
83
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/nitro.ts"],"sourcesContent":["import type { AnyExtension } from '@tiptap/core'\nimport { type AiModels, type AiModelsConfig, type CollabConnection, type CollabServer, type CollabServerOptions, createAiModels, createCollabServer, createRateLimiter, type EditorPorts, type RateLimiter, type RateLimiterOptions } from '../server'\nimport { defineEditorConfig } from '../config'\nimport { routeOf } from '../server/h3'\nimport type { AiHandlerContext, HandlerContext } from '../server/handlers'\nimport { adoptEditorServer } from '../server/host'\nimport { serverRoutesOf } from '../server/routes'\nimport { defineWebSocketHandler, type Router } from 'h3'\nimport { COLLAB_PATH } from '../core/api-routes'\n\n// The editor backend on a Nitro app: the routes the ports can serve (`serverRoutesOf`: settings, users, tags, assets,\n// themes, documents, comments, the optional AI endpoints) and the collaboration websocket, registered in one call from a\n// Nitro plugin. One installer serves both hosts: a plain Nitro app (nitropack 2) calls it from its own plugin, the\n// Nuxt module (`@ecsplico/editor/nuxt`) from its server plugin. Both end in the same `@ecsplico/editor/server/handlers`.\n\n/** The part of a Nitro app this entry uses; `nitroApp` of a Nitro plugin fits. */\nexport interface NitroHost {\n router: Pick<Router, 'add' | 'use'>\n hooks: { hook(name: 'close', listener: () => void | Promise<void>): unknown }\n}\n\nexport interface EditorServerOptions {\n /** Identity, storage and everything else the backend serves, one port per capability: a route is mounted only when its port is present. A documents port that also manages documents (`json`, `drizzle`) lets `PUT /api/editor/documents/:id` create them; the `comments` port mounts the comments endpoint, `models` the host's language models. */\n ports: EditorPorts\n /**\n * The collaboration websocket (`/collab/:id`). Schema, hooks and Hocuspocus configuration of `createCollabServer`;\n * `{}` takes everything from the ports (the `collabState` port is required). `false` mounts no websocket (documents then only load and save over HTTP).\n * `authenticateUpgrade`: authenticate the upgrade request itself (`Identity.authenticate` with its headers; cookies,\n * not the provider's `token`) and refuse it with HTTP 401, instead of closing an unauthenticated socket with 4401\n * after the upgrade; the user then travels with the connection and is not authenticated a second time.\n * Needs `experimental: { websocket: true }` in the Nitro config.\n */\n collab: (Omit<CollabServerOptions, 'ports'> & { authenticateUpgrade?: boolean }) | false\n /**\n * Mounts the AI endpoints (`/api/ai/*`): presets, shared key, transcription, slots of `createAiModels`; `languageModel`\n * and `listModels` default to those of the `models` port. Omitted: no AI routes.\n */\n ai?: Partial<AiModelsConfig>\n /** Per-user limit of the AI endpoints; default: that of `defineEditorConfig` (30 requests per minute). */\n rateLimit?: RateLimiterOptions\n /** The collaboration schema (see `CollabServerOptions.schema`); default `advancedSchemaExtensions()`, host nodes: `() => [...advancedSchemaExtensions(), Callout]`. */\n schema?: () => AnyExtension[]\n}\n\nexport interface EditorServer {\n ports: EditorPorts\n /** The collaboration server, `undefined` with `collab: false`. */\n collab: CollabServer | undefined\n /** The AI models, `undefined` without `ai`. */\n models: AiModels | undefined\n /** Writes pending collaboration stores and disconnects every client. Nitro's `close` hook calls it. */\n close(): Promise<void>\n}\n\ntype CollabPeer = { _hocuspocus?: CollabConnection }\n\n// Same crossws wiring as Hocuspocus' own Server; the document name comes from the provider's `name`. By default the user\n// is authenticated in Hocuspocus' `onAuthenticate` (cookie headers of the upgrade request or the provider's `token`), so\n// the upgrade always succeeds and an unauthenticated socket is closed with 4401 afterwards. With `authenticateUpgrade` the\n// upgrade request itself is authenticated (headers only) and refused with HTTP 401.\nconst collabRoute = (collab: CollabServer, ports: EditorPorts, authenticateUpgrade: boolean) => defineWebSocketHandler({\n async upgrade(request) {\n if (!authenticateUpgrade) return\n const user = await ports.identity.authenticate({ headers: request.headers })\n // No body: crossws' node adapter writes the response headers percent-encoded, which would mangle an automatic `content-type`.\n if (!user) return new Response(null, { status: 401, statusText: 'Unauthorized' })\n // crossws creates the context before it calls the hook (its type is optional only for other callers); `open` reads it as `peer.context`.\n request.context!.user = user\n },\n open(peer) {\n const user = peer.context.user as object | undefined\n ;(peer as unknown as CollabPeer)._hocuspocus = collab.handleConnection(peer.websocket as unknown as Parameters<CollabServer['handleConnection']>[0], peer.request as Request, user && { user })\n },\n message(peer, message) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleMessage(message.uint8Array())\n },\n close(peer, details) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleClose({ code: details.code ?? 1000, reason: details.reason ?? '' } as CloseEvent)\n },\n error(peer, error) {\n console.error('[collab]', peer.id, error)\n },\n})\n\n/**\n * Wires the editor backend into `nitroApp` (call it from a Nitro plugin):\n *\n * /api/editor/{capabilities,settings,users,tags,assets/…,themes/…} the editor's own endpoints, each with its port; `capabilities` lists what is served\n * /api/editor/documents/:id GET/PUT a document's TipTap JSON (what `useEditorDocument` uses)\n * /api/editor/comments/:id GET/POST comments, with `ports.comments`\n * /api/ai/{models,chat,complete,generate,extract,transcript-cleanup} with `ai`\n * /collab/:id the collaboration websocket, unless `collab: false`\n *\n * Every route authenticates through `ports.identity.authenticate`. The collaboration server is closed with Nitro's `close`\n * hook. The installed server is also the process-wide one of `@ecsplico/editor/server` (`useCollabServer`, `useAiModels`,\n * `liveContent`, …) until it closes. The paths are the ones the editor's client (`createHttpBackend`) calls.\n */\nexport function installEditorServer(nitroApp: NitroHost, options: EditorServerOptions): EditorServer {\n const { ports, schema } = options\n const { authenticateUpgrade = false, ...collabOptions }: Exclude<EditorServerOptions['collab'], false> = options.collab || {}\n const collab = options.collab === false ? undefined : createCollabServer({ ports, ...(schema && { schema }), ...collabOptions })\n const provider = ports.models\n const models = options.ai && createAiModels({\n presets: {},\n languageModel: provider?.languageModel && ((ref, user) => provider.languageModel!(ref, user)),\n listModels: provider?.listModels && (user => provider.listModels!(user)),\n ...options.ai,\n })\n const limiter: RateLimiter | undefined = models && createRateLimiter(options.rateLimit ?? defineEditorConfig().ai.rateLimit)\n\n const context: HandlerContext = { ports, ...(collab && { collab }), served: { ai: models !== undefined, collaboration: collab !== undefined } }\n const aiContext: AiHandlerContext | undefined = models && limiter && { ...context, ai: models, limiter }\n for (const route of serverRoutesOf(ports, { ai: aiContext !== undefined })) {\n // `serverRoutesOf` returns the AI routes only with `ai`, so `aiContext` is set for them.\n const handler = route.port === 'ai' ? routeOf(aiContext!, route.handler) : routeOf(context, route.handler)\n nitroApp.router.add(route.path, handler, route.method)\n }\n // No method: the websocket upgrade resolves its handler by path only.\n if (collab) nitroApp.router.use(`${COLLAB_PATH}/:id`, collabRoute(collab, ports, authenticateUpgrade))\n\n const release = adoptEditorServer({ ports, schema, collab, models, limiter })\n const server: EditorServer = {\n ports,\n collab,\n models,\n async close() {\n await collab?.close()\n release()\n },\n }\n nitroApp.hooks.hook('close', () => server.close())\n return server\n}\n"],"mappings":";;;;;;;;;;AA4DA,MAAM,eAAe,QAAsB,OAAoB,wBAAiC,uBAAuB;CACrH,MAAM,QAAQ,SAAS;EACrB,IAAI,CAAC,qBAAqB;EAC1B,MAAM,OAAO,MAAM,MAAM,SAAS,aAAa,EAAE,SAAS,QAAQ,QAAQ,CAAC;EAE3E,IAAI,CAAC,MAAM,OAAO,IAAI,SAAS,MAAM;GAAE,QAAQ;GAAK,YAAY;EAAe,CAAC;EAEhF,QAAQ,QAAS,OAAO;CAC1B;CACA,KAAK,MAAM;EACT,MAAM,OAAO,KAAK,QAAQ;EACzB,KAAgC,cAAc,OAAO,iBAAiB,KAAK,WAAyE,KAAK,SAAoB,QAAQ,EAAE,KAAK,CAAC;CAChM;CACA,QAAQ,MAAM,SAAS;EACpB,KAAgC,aAAa,cAAc,QAAQ,WAAW,CAAC;CAClF;CACA,MAAM,MAAM,SAAS;EAClB,KAAgC,aAAa,YAAY;GAAE,MAAM,QAAQ,QAAQ;GAAM,QAAQ,QAAQ,UAAU;EAAG,CAAe;CACtI;CACA,MAAM,MAAM,OAAO;EACjB,QAAQ,MAAM,YAAY,KAAK,IAAI,KAAK;CAC1C;AACF,CAAC;;;;;;;;;;;;;;AAeD,SAAgB,oBAAoB,UAAqB,SAA4C;CACnG,MAAM,EAAE,OAAO,WAAW;CAC1B,MAAM,EAAE,sBAAsB,OAAO,GAAG,kBAAiE,QAAQ,UAAU,CAAC;CAC5H,MAAM,SAAS,QAAQ,WAAW,QAAQ,KAAA,IAAY,mBAAmB;EAAE;EAAO,GAAI,UAAU,EAAE,OAAO;EAAI,GAAG;CAAc,CAAC;CAC/H,MAAM,WAAW,MAAM;CACvB,MAAM,SAAS,QAAQ,MAAM,eAAe;EAC1C,SAAS,CAAC;EACV,eAAe,UAAU,mBAAmB,KAAK,SAAS,SAAS,cAAe,KAAK,IAAI;EAC3F,YAAY,UAAU,gBAAe,SAAQ,SAAS,WAAY,IAAI;EACtE,GAAG,QAAQ;CACb,CAAC;CACD,MAAM,UAAmC,UAAU,kBAAkB,QAAQ,aAAa,mBAAmB,CAAC,CAAC,GAAG,SAAS;CAE3H,MAAM,UAA0B;EAAE;EAAO,GAAI,UAAU,EAAE,OAAO;EAAI,QAAQ;GAAE,IAAI,WAAW,KAAA;GAAW,eAAe,WAAW,KAAA;EAAU;CAAE;CAC9I,MAAM,YAA0C,UAAU,WAAW;EAAE,GAAG;EAAS,IAAI;EAAQ;CAAQ;CACvG,KAAK,MAAM,SAAS,eAAe,OAAO,EAAE,IAAI,cAAc,KAAA,EAAU,CAAC,GAAG;EAE1E,MAAM,UAAU,MAAM,SAAS,OAAO,QAAQ,WAAY,MAAM,OAAO,IAAI,QAAQ,SAAS,MAAM,OAAO;EACzG,SAAS,OAAO,IAAI,MAAM,MAAM,SAAS,MAAM,MAAM;CACvD;CAEA,IAAI,QAAQ,SAAS,OAAO,IAAI,GAAG,YAAY,OAAO,YAAY,QAAQ,OAAO,mBAAmB,CAAC;CAErG,MAAM,UAAU,kBAAkB;EAAE;EAAO;EAAQ;EAAQ;EAAQ;CAAQ,CAAC;CAC5E,MAAM,SAAuB;EAC3B;EACA;EACA;EACA,MAAM,QAAQ;GACZ,MAAM,QAAQ,MAAM;GACpB,QAAQ;EACV;CACF;CACA,SAAS,MAAM,KAAK,eAAe,OAAO,MAAM,CAAC;CACjD,OAAO;AACT"}
1
+ {"version":3,"file":"nitro.js","names":[],"sources":["../../src/adapters/nitro.ts"],"sourcesContent":["import type { AnyExtension } from '@tiptap/core'\nimport { type AiModels, type AiModelsConfig, type CollabConnection, type CollabServer, type CollabServerOptions, createAiModels, createCollabServer, createRateLimiter, type EditorPorts, type RateLimiter, type RateLimiterOptions } from '../server'\nimport { defineEditorConfig } from '../config'\nimport { routeOf } from '../server/h3'\nimport type { AiHandlerContext, HandlerContext } from '../server/handlers'\nimport { adoptEditorServer } from '../server/host'\nimport { type RouteDef, serverRoutesOf } from '../server/routes'\nimport { defineWebSocketHandler, type Router } from 'h3'\nimport { COLLAB_PATH } from '../core/api-routes'\n\n// The editor backend on a Nitro app: the routes the ports can serve (`serverRoutesOf`: settings, users, tags, assets,\n// themes, documents, comments, the optional AI endpoints) and the collaboration websocket, registered in one call from a\n// Nitro plugin. One installer serves both hosts: a plain Nitro app (nitropack 2) calls it from its own plugin, the\n// Nuxt module (`@ecsplico/editor/nuxt`) from its server plugin. Both end in the same `@ecsplico/editor/server/handlers`.\n\n/** The part of a Nitro app this entry uses; `nitroApp` of a Nitro plugin fits. */\nexport interface NitroHost {\n router: Pick<Router, 'add' | 'use'>\n hooks: { hook(name: 'close', listener: () => void | Promise<void>): unknown }\n}\n\n/** The route type of the table `EditorServerOptions.routes` filters. */\nexport type { RouteDef }\n\nexport interface EditorServerOptions {\n /** Identity, storage and everything else the backend serves, one port per capability: a route is mounted only when its port is present. A documents port that also manages documents (`json`, `drizzle`) lets `PUT /api/editor/documents/:id` create them; the `comments` port mounts the comments endpoint, `models` the host's language models. */\n ports: EditorPorts\n /**\n * The collaboration websocket (`/collab/:id`). Schema, hooks and Hocuspocus configuration of `createCollabServer`;\n * `{}` takes everything from the ports (the `collabState` port is required). `false` mounts no websocket (documents then only load and save over HTTP).\n * `authenticateUpgrade`: authenticate the upgrade request itself (`Identity.authenticate` with its headers; cookies,\n * not the provider's `token`) and refuse it with HTTP 401, instead of closing an unauthenticated socket with 4401\n * after the upgrade; the user then travels with the connection and is not authenticated a second time.\n * Needs `experimental: { websocket: true }` in the Nitro config.\n */\n collab: (Omit<CollabServerOptions, 'ports'> & { authenticateUpgrade?: boolean }) | false\n /**\n * Mounts the AI endpoints (`/api/ai/*`): presets, shared key, transcription, slots of `createAiModels`; `languageModel`\n * and `listModels` default to those of the `models` port. Omitted: no AI routes.\n */\n ai?: Partial<AiModelsConfig>\n /** Per-user limit of the AI endpoints; default: that of `defineEditorConfig` (30 requests per minute). */\n rateLimit?: RateLimiterOptions\n /** Filters or reorders the routes before they are mounted (a host that serves `/api/editor/assets/*` itself drops them); default: all. Receives what `serverRoutesOf(ports, { ai })` returns, so a route of an absent port, or an AI route without the `ai` option, is not in it. */\n routes?: (routes: readonly RouteDef[]) => readonly RouteDef[]\n /** The collaboration schema (see `CollabServerOptions.schema`); default `advancedSchemaExtensions()`, host nodes: `() => [...advancedSchemaExtensions(), Callout]`. */\n schema?: () => AnyExtension[]\n}\n\nexport interface EditorServer {\n ports: EditorPorts\n /** The collaboration server, `undefined` with `collab: false`. */\n collab: CollabServer | undefined\n /** The AI models, `undefined` without `ai`. */\n models: AiModels | undefined\n /** Writes pending collaboration stores and disconnects every client. Nitro's `close` hook calls it. */\n close(): Promise<void>\n}\n\ntype CollabPeer = { _hocuspocus?: CollabConnection }\n\n// Same crossws wiring as Hocuspocus' own Server; the document name comes from the provider's `name`. By default the user\n// is authenticated in Hocuspocus' `onAuthenticate` (cookie headers of the upgrade request or the provider's `token`), so\n// the upgrade always succeeds and an unauthenticated socket is closed with 4401 afterwards. With `authenticateUpgrade` the\n// upgrade request itself is authenticated (headers only) and refused with HTTP 401.\nconst collabRoute = (collab: CollabServer, ports: EditorPorts, authenticateUpgrade: boolean) => defineWebSocketHandler({\n async upgrade(request) {\n if (!authenticateUpgrade) return\n const user = await ports.identity.authenticate({ headers: request.headers })\n // No body: crossws' node adapter writes the response headers percent-encoded, which would mangle an automatic `content-type`.\n if (!user) return new Response(null, { status: 401, statusText: 'Unauthorized' })\n // crossws creates the context before it calls the hook (its type is optional only for other callers); `open` reads it as `peer.context`.\n request.context!.user = user\n },\n open(peer) {\n const user = peer.context.user as object | undefined\n ;(peer as unknown as CollabPeer)._hocuspocus = collab.handleConnection(peer.websocket as unknown as Parameters<CollabServer['handleConnection']>[0], peer.request as Request, user && { user })\n },\n message(peer, message) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleMessage(message.uint8Array())\n },\n close(peer, details) {\n ;(peer as unknown as CollabPeer)._hocuspocus?.handleClose({ code: details.code ?? 1000, reason: details.reason ?? '' } as CloseEvent)\n },\n error(peer, error) {\n console.error('[collab]', peer.id, error)\n },\n})\n\n/**\n * Wires the editor backend into `nitroApp` (call it from a Nitro plugin):\n *\n * /api/editor/{capabilities,settings,users,tags,assets/…,themes/…} the editor's own endpoints, each with its port; `capabilities` lists what is served\n * /api/editor/documents/:id GET/PUT a document's TipTap JSON (what `useEditorDocument` uses)\n * /api/editor/comments/:id GET/POST comments, with `ports.comments`\n * /api/ai/{models,chat,complete,generate,extract,transcript-cleanup} with `ai`\n * /collab/:id the collaboration websocket, unless `collab: false`\n *\n * Every route authenticates through `ports.identity.authenticate`. The collaboration server is closed with Nitro's `close`\n * hook. The installed server is also the process-wide one of `@ecsplico/editor/server` (`useCollabServer`, `useAiModels`,\n * `liveContent`, …) until it closes. The paths are the ones the editor's client (`createHttpBackend`) calls.\n */\nexport function installEditorServer(nitroApp: NitroHost, options: EditorServerOptions): EditorServer {\n const { ports, schema } = options\n const { authenticateUpgrade = false, ...collabOptions }: Exclude<EditorServerOptions['collab'], false> = options.collab || {}\n const collab = options.collab === false ? undefined : createCollabServer({ ports, ...(schema && { schema }), ...collabOptions })\n const provider = ports.models\n const models = options.ai && createAiModels({\n presets: {},\n languageModel: provider?.languageModel && ((ref, user) => provider.languageModel!(ref, user)),\n listModels: provider?.listModels && (user => provider.listModels!(user)),\n ...options.ai,\n })\n const limiter: RateLimiter | undefined = models && createRateLimiter(options.rateLimit ?? defineEditorConfig().ai.rateLimit)\n\n const context: HandlerContext = { ports, ...(collab && { collab }), served: { ai: models !== undefined, collaboration: collab !== undefined } }\n const aiContext: AiHandlerContext | undefined = models && limiter && { ...context, ai: models, limiter }\n const mounted = serverRoutesOf(ports, { ai: aiContext !== undefined })\n for (const route of options.routes ? options.routes(mounted) : mounted) {\n // `serverRoutesOf` returns the AI routes only with `ai`, so `aiContext` is set for them.\n const handler = route.port === 'ai' ? routeOf(aiContext!, route.handler) : routeOf(context, route.handler)\n nitroApp.router.add(route.path, handler, route.method)\n }\n // No method: the websocket upgrade resolves its handler by path only.\n if (collab) nitroApp.router.use(`${COLLAB_PATH}/:id`, collabRoute(collab, ports, authenticateUpgrade))\n\n const release = adoptEditorServer({ ports, schema, collab, models, limiter })\n const server: EditorServer = {\n ports,\n collab,\n models,\n async close() {\n await collab?.close()\n release()\n },\n }\n nitroApp.hooks.hook('close', () => server.close())\n return server\n}\n"],"mappings":";;;;;;;;;;AAiEA,MAAM,eAAe,QAAsB,OAAoB,wBAAiC,uBAAuB;CACrH,MAAM,QAAQ,SAAS;EACrB,IAAI,CAAC,qBAAqB;EAC1B,MAAM,OAAO,MAAM,MAAM,SAAS,aAAa,EAAE,SAAS,QAAQ,QAAQ,CAAC;EAE3E,IAAI,CAAC,MAAM,OAAO,IAAI,SAAS,MAAM;GAAE,QAAQ;GAAK,YAAY;EAAe,CAAC;EAEhF,QAAQ,QAAS,OAAO;CAC1B;CACA,KAAK,MAAM;EACT,MAAM,OAAO,KAAK,QAAQ;EACzB,KAAgC,cAAc,OAAO,iBAAiB,KAAK,WAAyE,KAAK,SAAoB,QAAQ,EAAE,KAAK,CAAC;CAChM;CACA,QAAQ,MAAM,SAAS;EACpB,KAAgC,aAAa,cAAc,QAAQ,WAAW,CAAC;CAClF;CACA,MAAM,MAAM,SAAS;EAClB,KAAgC,aAAa,YAAY;GAAE,MAAM,QAAQ,QAAQ;GAAM,QAAQ,QAAQ,UAAU;EAAG,CAAe;CACtI;CACA,MAAM,MAAM,OAAO;EACjB,QAAQ,MAAM,YAAY,KAAK,IAAI,KAAK;CAC1C;AACF,CAAC;;;;;;;;;;;;;;AAeD,SAAgB,oBAAoB,UAAqB,SAA4C;CACnG,MAAM,EAAE,OAAO,WAAW;CAC1B,MAAM,EAAE,sBAAsB,OAAO,GAAG,kBAAiE,QAAQ,UAAU,CAAC;CAC5H,MAAM,SAAS,QAAQ,WAAW,QAAQ,KAAA,IAAY,mBAAmB;EAAE;EAAO,GAAI,UAAU,EAAE,OAAO;EAAI,GAAG;CAAc,CAAC;CAC/H,MAAM,WAAW,MAAM;CACvB,MAAM,SAAS,QAAQ,MAAM,eAAe;EAC1C,SAAS,CAAC;EACV,eAAe,UAAU,mBAAmB,KAAK,SAAS,SAAS,cAAe,KAAK,IAAI;EAC3F,YAAY,UAAU,gBAAe,SAAQ,SAAS,WAAY,IAAI;EACtE,GAAG,QAAQ;CACb,CAAC;CACD,MAAM,UAAmC,UAAU,kBAAkB,QAAQ,aAAa,mBAAmB,CAAC,CAAC,GAAG,SAAS;CAE3H,MAAM,UAA0B;EAAE;EAAO,GAAI,UAAU,EAAE,OAAO;EAAI,QAAQ;GAAE,IAAI,WAAW,KAAA;GAAW,eAAe,WAAW,KAAA;EAAU;CAAE;CAC9I,MAAM,YAA0C,UAAU,WAAW;EAAE,GAAG;EAAS,IAAI;EAAQ;CAAQ;CACvG,MAAM,UAAU,eAAe,OAAO,EAAE,IAAI,cAAc,KAAA,EAAU,CAAC;CACrE,KAAK,MAAM,SAAS,QAAQ,SAAS,QAAQ,OAAO,OAAO,IAAI,SAAS;EAEtE,MAAM,UAAU,MAAM,SAAS,OAAO,QAAQ,WAAY,MAAM,OAAO,IAAI,QAAQ,SAAS,MAAM,OAAO;EACzG,SAAS,OAAO,IAAI,MAAM,MAAM,SAAS,MAAM,MAAM;CACvD;CAEA,IAAI,QAAQ,SAAS,OAAO,IAAI,GAAG,YAAY,OAAO,YAAY,QAAQ,OAAO,mBAAmB,CAAC;CAErG,MAAM,UAAU,kBAAkB;EAAE;EAAO;EAAQ;EAAQ;EAAQ;CAAQ,CAAC;CAC5E,MAAM,SAAuB;EAC3B;EACA;EACA;EACA,MAAM,QAAQ;GACZ,MAAM,QAAQ,MAAM;GACpB,QAAQ;EACV;CACF;CACA,SAAS,MAAM,KAAK,eAAe,OAAO,MAAM,CAAC;CACjD,OAAO;AACT"}
@@ -11,12 +11,12 @@ type __VLS_Props = {
11
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
12
12
  view: import("vue").ShallowRef<EditorView | undefined, EditorView | undefined>;
13
13
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
14
- "update:modelValue": (text: string) => any;
15
14
  ready: (view: EditorView) => any;
15
+ "update:modelValue": (text: string) => any;
16
16
  save: (text: string) => any;
17
17
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
18
- "onUpdate:modelValue"?: ((text: string) => any) | undefined;
19
18
  onReady?: ((view: EditorView) => any) | undefined;
19
+ "onUpdate:modelValue"?: ((text: string) => any) | undefined;
20
20
  onSave?: ((text: string) => any) | undefined;
21
21
  }>, {
22
22
  editable: boolean;
@@ -1,6 +1,7 @@
1
1
  import { MessageKey } from "../i18n/index.js";
2
2
  import { Ability } from "./schema/abilities.js";
3
3
  import { Editor } from "@tiptap/core";
4
+ import { EditorState, Transaction } from "@tiptap/pm/state";
4
5
  import { ResolvedPos } from "@tiptap/pm/model";
5
6
  //#region src/core/command-types.d.ts
6
7
  /** One row of a dropdown a command or block action opens; the framework layer renders it. */
@@ -80,6 +81,12 @@ export interface PlacedToolbarGroup<C = unknown> extends ToolbarGroupDef {
80
81
  * that would receive it. Features contribute guards (`EditorFeature.insertGuards`), e.g. the accepted children of a Baustein.
81
82
  */
82
83
  export type InsertGuard = (editor: Editor, $parent: ResolvedPos, creates: Creates) => boolean;
84
+ /**
85
+ * Judges a whole transaction before it is applied (`EditorFeature.transactionGuards`). `true` lets it pass; a string
86
+ * refuses the whole transaction and is the notice shown to the user. Only transactions that change the document are
87
+ * judged. `state` is the state before `tr`; `tr.doc` is the document after it.
88
+ */
89
+ export type TransactionGuard = (tr: Transaction, state: EditorState) => true | string;
83
90
  export interface CommandState {
84
91
  visible: boolean;
85
92
  enabled: boolean;
@@ -1,4 +1,4 @@
1
- import { CommandBase, CommandDef, CommandState, Creates, InsertGuard, MenuItem, PlacedToolbarGroup, ToolbarGroupDef } from "./command-types.js";
1
+ import { CommandBase, CommandDef, CommandState, Creates, InsertGuard, MenuItem, PlacedToolbarGroup, ToolbarGroupDef, TransactionGuard } from "./command-types.js";
2
2
  import { EditorPreset, FeatureContext } from "./feature.js";
3
3
  import { ChainedCommands, Editor } from "@tiptap/core";
4
4
  import { ResolvedPos } from "@tiptap/pm/model";
@@ -36,5 +36,5 @@ export declare function commandsOf<C>(preset: EditorPreset<C>, ctx: FeatureConte
36
36
  */
37
37
  export declare function toolbarOf<C>(preset: EditorPreset<C>, ctx: FeatureContext): PlacedToolbarGroup<C>[];
38
38
  //#endregion
39
- export type { CommandBase, CommandDef, CommandState, Creates, InsertGuard, MenuItem, PlacedToolbarGroup, ToolbarGroupDef };
39
+ export type { CommandBase, CommandDef, CommandState, Creates, InsertGuard, MenuItem, PlacedToolbarGroup, ToolbarGroupDef, TransactionGuard };
40
40
  //# sourceMappingURL=commands.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"commands.js","names":[],"sources":["../../src/core/commands.ts"],"sourcesContent":["import type { ChainedCommands, Editor } from '@tiptap/core'\nimport type { ResolvedPos } from '@tiptap/pm/model'\nimport { abilitiesAt, abilitiesAtSelection } from './abilities'\nimport { contributions } from './define-editor'\nimport { type EditorEntry, editorEntryOf } from './editor-ui'\nimport type { EditorPreset, FeatureContext } from './feature'\nimport type { CommandBase, CommandDef, CommandState, Creates, InsertGuard, PlacedToolbarGroup, ToolbarGroupDef } from './command-types'\nimport { byOrder, fail } from './util'\n\nexport type { CommandBase, CommandDef, CommandState, Creates, InsertGuard, MenuItem, PlacedToolbarGroup, ToolbarGroupDef } from './command-types'\n\n// The guards of an editor are the `insertGuards` of the features of the preset its surface registered\n// (`registerEditor`): looked up by the raw editor instance, built once per entry. An editor without an entry\n// (a headless test editor) has none. Callers holding a framework proxy of the editor pass the raw instance.\nconst guardCache = new WeakMap<EditorEntry, readonly InsertGuard[]>()\n\nfunction guardsOf(editor: Editor): readonly InsertGuard[] {\n const entry = editorEntryOf(editor)\n if (!entry) return []\n let guards = guardCache.get(entry)\n if (!guards) {\n guards = contributions(entry.preset, 'insertGuards').flatMap(factory => factory(entry.ctx))\n guardCache.set(entry, guards)\n }\n return guards\n}\n\n/**\n * A command over a command chain: `run` focuses the editor and executes `command`; the command is disabled while\n * `can()` says it is structurally impossible (also an empty undo history). With `creates`, `commandState` additionally\n * requires the node to fit at the caret.\n */\nexport function command(def: Omit<CommandBase, 'can'> & { command: (chain: ChainedCommands) => ChainedCommands }): CommandDef<never> {\n const { command: chainOf, ...rest } = def\n return {\n ...rest,\n can: editor => chainOf(editor.can().chain()).run(),\n run: (editor) => { chainOf(editor.chain().focus()).run() },\n }\n}\n\n/**\n * Whether every registered insert guard accepts `creates` below the ancestor `$parent` stands for (`$parent.parent` is the\n * node that would receive it). The one rule behind both creation (`canCreateAt`) and conversion targets (`convertBlockTo`).\n */\nexport function guardsAccept(editor: Editor, $parent: ResolvedPos, creates: Creates): boolean {\n return guardsOf(editor).every(guard => guard(editor, $parent, creates))\n}\n\n/**\n * Whether a node like `creates` may be inserted at `$pos`: walks from the caret's depth outward to the first ancestor\n * whose content accepts the type at the caret's index, then every registered insert guard must accept it there.\n */\nexport function canCreateAt(editor: Editor, $pos: ResolvedPos, creates: Creates): boolean {\n const type = editor.schema.nodes[creates.type]\n if (!type) return false\n for (let depth = $pos.depth; depth >= 0; depth--) {\n const index = $pos.index(depth)\n if (!$pos.node(depth).canReplaceWith(index, index, type)) continue\n return guardsAccept(editor, $pos.doc.resolve($pos.start(depth)), creates)\n }\n return false\n}\n\n/**\n * The state of `cmd` for the toolbar (`'selection'`: abilities of the selection) or the `/` menu (`'caret'`). `$at`\n * replaces the caret position (the `/` plugin evaluates inside a transaction's `apply`, where `editor.state` is still\n * the old state); `can` hooks always read `editor.state`.\n */\nexport function commandState(editor: Editor, cmd: CommandDef, at: 'selection' | 'caret', $at?: ResolvedPos): CommandState {\n const visible = cmd.visible?.(editor) ?? true\n if ('component' in cmd) return { visible, enabled: true, active: false }\n const active = cmd.active?.(editor) ?? false\n if (active) return { visible, enabled: true, active }\n const $from = $at ?? editor.state.selection.$from\n if (cmd.ability) {\n const abilities = at === 'selection' && !$at ? abilitiesAtSelection(editor) : abilitiesAt(editor, $from)\n if (!abilities.has(cmd.ability)) return { visible, enabled: false, active }\n }\n const fits = (!cmd.can || cmd.can(editor)) && (!cmd.creates || canCreateAt(editor, $from, cmd.creates))\n return { visible, enabled: fits, active }\n}\n\n/** The commands of the preset's features in feature order. Throws when two commands share an id. */\nexport function commandsOf<C>(preset: EditorPreset<C>, ctx: FeatureContext): CommandDef<C>[] {\n return collect(preset, ctx).commands\n}\n\n// The feature each command came from, for error messages; commands of the host's `commands` transformer have none.\nconst ownerOfCommand = new WeakMap<object, string>()\n\n/**\n * Throws when two of `commands` share an id (`CommandBase.id` is unique per editor), naming the declaring features;\n * a command without a known feature is the host's.\n */\nexport function assertUniqueCommandIds<C>(commands: readonly CommandDef<C>[]): void {\n const seen = new Map<string, CommandDef<C>>()\n for (const cmd of commands) {\n const first = seen.get(cmd.id)\n if (first) fail(`command \"${cmd.id}\" is declared by \"${ownerOfCommand.get(first) ?? 'host'}\" and \"${ownerOfCommand.get(cmd) ?? 'host'}\"`)\n seen.set(cmd.id, cmd)\n }\n}\n\n/**\n * Places `commands` into their toolbar `groups` (every declared group, also empty ones, sorted by `order`; a group's\n * commands sorted by `toolbar.order`, stable). `unplaced` receives each command that has no group to go to: no `toolbar`\n * placement, or one naming a group that is not declared; what that means is the caller's policy.\n */\nexport function placeInGroups<C>(groups: readonly ToolbarGroupDef[], commands: readonly CommandDef<C>[], unplaced: (cmd: CommandDef<C>) => void): PlacedToolbarGroup<C>[] {\n const placed = new Map<string, PlacedToolbarGroup<C>>()\n // `defineEditor` guarantees every group id is declared once.\n for (const group of groups) placed.set(group.id, { ...group, items: [] })\n for (const cmd of commands) {\n const group = cmd.toolbar ? placed.get(cmd.toolbar.group) : undefined\n if (group) group.items.push(cmd)\n else unplaced(cmd)\n }\n return [...placed.values()]\n .sort(byOrder)\n .map(group => ({ ...group, items: [...group.items].sort((a, b) => byOrder(a.toolbar || {}, b.toolbar || {})) }))\n}\n\n/** The toolbar groups the features of `preset` declare, in feature order. */\nexport const toolbarGroupsOf = <C>(preset: EditorPreset<C>): ToolbarGroupDef[] => preset.features.flatMap(feature => feature.toolbarGroups ?? [])\n\n/**\n * The toolbar of `preset`: the groups features declare (`toolbarGroups`) with every command that has a `toolbar`\n * placement, groups and commands sorted by `order` (stable). Throws when a command names a group no feature declares\n * or two commands share an id.\n */\nexport function toolbarOf<C>(preset: EditorPreset<C>, ctx: FeatureContext): PlacedToolbarGroup<C>[] {\n const { commands, groups } = collect(preset, ctx)\n return placeInGroups(groups, commands, (cmd) => {\n if (cmd.toolbar) fail(`command \"${cmd.id}\" of \"${ownerOfCommand.get(cmd) ?? '?'}\" names toolbar group \"${cmd.toolbar.group}\" that no feature declares`)\n })\n}\n\nfunction collect<C>(preset: EditorPreset<C>, ctx: FeatureContext) {\n const commands: CommandDef<C>[] = []\n for (const feature of preset.features) {\n for (const cmd of feature.commands?.(ctx) ?? []) {\n commands.push(cmd)\n ownerOfCommand.set(cmd, feature.id)\n }\n }\n assertUniqueCommandIds(commands)\n return { commands, groups: toolbarGroupsOf(preset) }\n}\n"],"mappings":";;;;;AAcA,MAAM,6BAAa,IAAI,QAA6C;AAEpE,SAAS,SAAS,QAAwC;CACxD,MAAM,QAAQ,cAAc,MAAM;CAClC,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,IAAI,SAAS,WAAW,IAAI,KAAK;CACjC,IAAI,CAAC,QAAQ;EACX,SAAS,cAAc,MAAM,QAAQ,cAAc,CAAC,CAAC,SAAQ,YAAW,QAAQ,MAAM,GAAG,CAAC;EAC1F,WAAW,IAAI,OAAO,MAAM;CAC9B;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,QAAQ,KAA6G;CACnI,MAAM,EAAE,SAAS,SAAS,GAAG,SAAS;CACtC,OAAO;EACL,GAAG;EACH,MAAK,WAAU,QAAQ,OAAO,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;EACjD,MAAM,WAAW;GAAE,QAAQ,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;EAAE;CAC3D;AACF;;;;;AAMA,SAAgB,aAAa,QAAgB,SAAsB,SAA2B;CAC5F,OAAO,SAAS,MAAM,CAAC,CAAC,OAAM,UAAS,MAAM,QAAQ,SAAS,OAAO,CAAC;AACxE;;;;;AAMA,SAAgB,YAAY,QAAgB,MAAmB,SAA2B;CACxF,MAAM,OAAO,OAAO,OAAO,MAAM,QAAQ;CACzC,IAAI,CAAC,MAAM,OAAO;CAClB,KAAK,IAAI,QAAQ,KAAK,OAAO,SAAS,GAAG,SAAS;EAChD,MAAM,QAAQ,KAAK,MAAM,KAAK;EAC9B,IAAI,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,eAAe,OAAO,OAAO,IAAI,GAAG;EAC1D,OAAO,aAAa,QAAQ,KAAK,IAAI,QAAQ,KAAK,MAAM,KAAK,CAAC,GAAG,OAAO;CAC1E;CACA,OAAO;AACT;;;;;;AAOA,SAAgB,aAAa,QAAgB,KAAiB,IAA2B,KAAiC;CACxH,MAAM,UAAU,IAAI,UAAU,MAAM,KAAK;CACzC,IAAI,eAAe,KAAK,OAAO;EAAE;EAAS,SAAS;EAAM,QAAQ;CAAM;CACvE,MAAM,SAAS,IAAI,SAAS,MAAM,KAAK;CACvC,IAAI,QAAQ,OAAO;EAAE;EAAS,SAAS;EAAM;CAAO;CACpD,MAAM,QAAQ,OAAO,OAAO,MAAM,UAAU;CAC5C,IAAI,IAAI,SAEF;MAAA,EADc,OAAO,eAAe,CAAC,MAAM,qBAAqB,MAAM,IAAI,YAAY,QAAQ,KAAK,EAAA,CACxF,IAAI,IAAI,OAAO,GAAG,OAAO;GAAE;GAAS,SAAS;GAAO;EAAO;CAAA;CAG5E,OAAO;EAAE;EAAS,UADJ,CAAC,IAAI,OAAO,IAAI,IAAI,MAAM,OAAO,CAAC,IAAI,WAAW,YAAY,QAAQ,OAAO,IAAI,OAAO;EACpE;CAAO;AAC1C;;AAGA,SAAgB,WAAc,QAAyB,KAAsC;CAC3F,OAAO,QAAQ,QAAQ,GAAG,CAAC,CAAC;AAC9B;AAGA,MAAM,iCAAiB,IAAI,QAAwB;;;;;AAMnD,SAAgB,uBAA0B,UAA0C;CAClF,MAAM,uBAAO,IAAI,IAA2B;CAC5C,KAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,QAAQ,KAAK,IAAI,IAAI,EAAE;EAC7B,IAAI,OAAO,KAAK,YAAY,IAAI,GAAG,oBAAoB,eAAe,IAAI,KAAK,KAAK,OAAO,SAAS,eAAe,IAAI,GAAG,KAAK,OAAO,EAAE;EACxI,KAAK,IAAI,IAAI,IAAI,GAAG;CACtB;AACF;;;;;;AAOA,SAAgB,cAAiB,QAAoC,UAAoC,UAAiE;CACxK,MAAM,yBAAS,IAAI,IAAmC;CAEtD,KAAK,MAAM,SAAS,QAAQ,OAAO,IAAI,MAAM,IAAI;EAAE,GAAG;EAAO,OAAO,CAAC;CAAE,CAAC;CACxE,KAAK,MAAM,OAAO,UAAU;EAC1B,MAAM,QAAQ,IAAI,UAAU,OAAO,IAAI,IAAI,QAAQ,KAAK,IAAI,KAAA;EAC5D,IAAI,OAAO,MAAM,MAAM,KAAK,GAAG;OAC1B,SAAS,GAAG;CACnB;CACA,OAAO,CAAC,GAAG,OAAO,OAAO,CAAC,CAAC,CACxB,KAAK,OAAO,CAAC,CACb,KAAI,WAAU;EAAE,GAAG;EAAO,OAAO,CAAC,GAAG,MAAM,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,QAAQ,EAAE,WAAW,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC;CAAE,EAAE;AACnH;;AAGA,MAAa,mBAAsB,WAA+C,OAAO,SAAS,SAAQ,YAAW,QAAQ,iBAAiB,CAAC,CAAC;;;;;;AAOhJ,SAAgB,UAAa,QAAyB,KAA8C;CAClG,MAAM,EAAE,UAAU,WAAW,QAAQ,QAAQ,GAAG;CAChD,OAAO,cAAc,QAAQ,WAAW,QAAQ;EAC9C,IAAI,IAAI,SAAS,KAAK,YAAY,IAAI,GAAG,QAAQ,eAAe,IAAI,GAAG,KAAK,IAAI,yBAAyB,IAAI,QAAQ,MAAM,2BAA2B;CACxJ,CAAC;AACH;AAEA,SAAS,QAAW,QAAyB,KAAqB;CAChE,MAAM,WAA4B,CAAC;CACnC,KAAK,MAAM,WAAW,OAAO,UAC3B,KAAK,MAAM,OAAO,QAAQ,WAAW,GAAG,KAAK,CAAC,GAAG;EAC/C,SAAS,KAAK,GAAG;EACjB,eAAe,IAAI,KAAK,QAAQ,EAAE;CACpC;CAEF,uBAAuB,QAAQ;CAC/B,OAAO;EAAE;EAAU,QAAQ,gBAAgB,MAAM;CAAE;AACrD"}
1
+ {"version":3,"file":"commands.js","names":[],"sources":["../../src/core/commands.ts"],"sourcesContent":["import type { ChainedCommands, Editor } from '@tiptap/core'\nimport type { ResolvedPos } from '@tiptap/pm/model'\nimport { abilitiesAt, abilitiesAtSelection } from './abilities'\nimport { contributions } from './define-editor'\nimport { type EditorEntry, editorEntryOf } from './editor-ui'\nimport type { EditorPreset, FeatureContext } from './feature'\nimport type { CommandBase, CommandDef, CommandState, Creates, InsertGuard, PlacedToolbarGroup, ToolbarGroupDef } from './command-types'\nimport { byOrder, fail } from './util'\n\nexport type { CommandBase, CommandDef, CommandState, Creates, InsertGuard, MenuItem, PlacedToolbarGroup, ToolbarGroupDef, TransactionGuard } from './command-types'\n\n// The guards of an editor are the `insertGuards` of the features of the preset its surface registered\n// (`registerEditor`): looked up by the raw editor instance, built once per entry. An editor without an entry\n// (a headless test editor) has none. Callers holding a framework proxy of the editor pass the raw instance.\nconst guardCache = new WeakMap<EditorEntry, readonly InsertGuard[]>()\n\nfunction guardsOf(editor: Editor): readonly InsertGuard[] {\n const entry = editorEntryOf(editor)\n if (!entry) return []\n let guards = guardCache.get(entry)\n if (!guards) {\n guards = contributions(entry.preset, 'insertGuards').flatMap(factory => factory(entry.ctx))\n guardCache.set(entry, guards)\n }\n return guards\n}\n\n/**\n * A command over a command chain: `run` focuses the editor and executes `command`; the command is disabled while\n * `can()` says it is structurally impossible (also an empty undo history). With `creates`, `commandState` additionally\n * requires the node to fit at the caret.\n */\nexport function command(def: Omit<CommandBase, 'can'> & { command: (chain: ChainedCommands) => ChainedCommands }): CommandDef<never> {\n const { command: chainOf, ...rest } = def\n return {\n ...rest,\n can: editor => chainOf(editor.can().chain()).run(),\n run: (editor) => { chainOf(editor.chain().focus()).run() },\n }\n}\n\n/**\n * Whether every registered insert guard accepts `creates` below the ancestor `$parent` stands for (`$parent.parent` is the\n * node that would receive it). The one rule behind both creation (`canCreateAt`) and conversion targets (`convertBlockTo`).\n */\nexport function guardsAccept(editor: Editor, $parent: ResolvedPos, creates: Creates): boolean {\n return guardsOf(editor).every(guard => guard(editor, $parent, creates))\n}\n\n/**\n * Whether a node like `creates` may be inserted at `$pos`: walks from the caret's depth outward to the first ancestor\n * whose content accepts the type at the caret's index, then every registered insert guard must accept it there.\n */\nexport function canCreateAt(editor: Editor, $pos: ResolvedPos, creates: Creates): boolean {\n const type = editor.schema.nodes[creates.type]\n if (!type) return false\n for (let depth = $pos.depth; depth >= 0; depth--) {\n const index = $pos.index(depth)\n if (!$pos.node(depth).canReplaceWith(index, index, type)) continue\n return guardsAccept(editor, $pos.doc.resolve($pos.start(depth)), creates)\n }\n return false\n}\n\n/**\n * The state of `cmd` for the toolbar (`'selection'`: abilities of the selection) or the `/` menu (`'caret'`). `$at`\n * replaces the caret position (the `/` plugin evaluates inside a transaction's `apply`, where `editor.state` is still\n * the old state); `can` hooks always read `editor.state`.\n */\nexport function commandState(editor: Editor, cmd: CommandDef, at: 'selection' | 'caret', $at?: ResolvedPos): CommandState {\n const visible = cmd.visible?.(editor) ?? true\n if ('component' in cmd) return { visible, enabled: true, active: false }\n const active = cmd.active?.(editor) ?? false\n if (active) return { visible, enabled: true, active }\n const $from = $at ?? editor.state.selection.$from\n if (cmd.ability) {\n const abilities = at === 'selection' && !$at ? abilitiesAtSelection(editor) : abilitiesAt(editor, $from)\n if (!abilities.has(cmd.ability)) return { visible, enabled: false, active }\n }\n const fits = (!cmd.can || cmd.can(editor)) && (!cmd.creates || canCreateAt(editor, $from, cmd.creates))\n return { visible, enabled: fits, active }\n}\n\n/** The commands of the preset's features in feature order. Throws when two commands share an id. */\nexport function commandsOf<C>(preset: EditorPreset<C>, ctx: FeatureContext): CommandDef<C>[] {\n return collect(preset, ctx).commands\n}\n\n// The feature each command came from, for error messages; commands of the host's `commands` transformer have none.\nconst ownerOfCommand = new WeakMap<object, string>()\n\n/**\n * Throws when two of `commands` share an id (`CommandBase.id` is unique per editor), naming the declaring features;\n * a command without a known feature is the host's.\n */\nexport function assertUniqueCommandIds<C>(commands: readonly CommandDef<C>[]): void {\n const seen = new Map<string, CommandDef<C>>()\n for (const cmd of commands) {\n const first = seen.get(cmd.id)\n if (first) fail(`command \"${cmd.id}\" is declared by \"${ownerOfCommand.get(first) ?? 'host'}\" and \"${ownerOfCommand.get(cmd) ?? 'host'}\"`)\n seen.set(cmd.id, cmd)\n }\n}\n\n/**\n * Places `commands` into their toolbar `groups` (every declared group, also empty ones, sorted by `order`; a group's\n * commands sorted by `toolbar.order`, stable). `unplaced` receives each command that has no group to go to: no `toolbar`\n * placement, or one naming a group that is not declared; what that means is the caller's policy.\n */\nexport function placeInGroups<C>(groups: readonly ToolbarGroupDef[], commands: readonly CommandDef<C>[], unplaced: (cmd: CommandDef<C>) => void): PlacedToolbarGroup<C>[] {\n const placed = new Map<string, PlacedToolbarGroup<C>>()\n // `defineEditor` guarantees every group id is declared once.\n for (const group of groups) placed.set(group.id, { ...group, items: [] })\n for (const cmd of commands) {\n const group = cmd.toolbar ? placed.get(cmd.toolbar.group) : undefined\n if (group) group.items.push(cmd)\n else unplaced(cmd)\n }\n return [...placed.values()]\n .sort(byOrder)\n .map(group => ({ ...group, items: [...group.items].sort((a, b) => byOrder(a.toolbar || {}, b.toolbar || {})) }))\n}\n\n/** The toolbar groups the features of `preset` declare, in feature order. */\nexport const toolbarGroupsOf = <C>(preset: EditorPreset<C>): ToolbarGroupDef[] => preset.features.flatMap(feature => feature.toolbarGroups ?? [])\n\n/**\n * The toolbar of `preset`: the groups features declare (`toolbarGroups`) with every command that has a `toolbar`\n * placement, groups and commands sorted by `order` (stable). Throws when a command names a group no feature declares\n * or two commands share an id.\n */\nexport function toolbarOf<C>(preset: EditorPreset<C>, ctx: FeatureContext): PlacedToolbarGroup<C>[] {\n const { commands, groups } = collect(preset, ctx)\n return placeInGroups(groups, commands, (cmd) => {\n if (cmd.toolbar) fail(`command \"${cmd.id}\" of \"${ownerOfCommand.get(cmd) ?? '?'}\" names toolbar group \"${cmd.toolbar.group}\" that no feature declares`)\n })\n}\n\nfunction collect<C>(preset: EditorPreset<C>, ctx: FeatureContext) {\n const commands: CommandDef<C>[] = []\n for (const feature of preset.features) {\n for (const cmd of feature.commands?.(ctx) ?? []) {\n commands.push(cmd)\n ownerOfCommand.set(cmd, feature.id)\n }\n }\n assertUniqueCommandIds(commands)\n return { commands, groups: toolbarGroupsOf(preset) }\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ import { EditorBackend } from "./backend.js";
6
6
  import { Notifier } from "./notify.js";
7
7
  import { EditorAiConfig, EditorContext } from "./context.js";
8
8
  import { ConvertTarget } from "./convert.js";
9
- import { CommandDef, InsertGuard, ToolbarGroupDef } from "./command-types.js";
9
+ import { CommandDef, InsertGuard, ToolbarGroupDef, TransactionGuard } from "./command-types.js";
10
10
  import { AbilitiesOptions, BlockAction } from "./extension-points.js";
11
11
  import { EditorUi } from "./editor-ui.js";
12
12
  import { EditorMetadata } from "./metadata-types.js";
@@ -146,7 +146,7 @@ export interface AiContextKind {
146
146
  fields: readonly {
147
147
  key: string;
148
148
  label: string;
149
- type?: 'text' | 'textarea' | 'date' | 'number' | 'select' | 'reference' | 'bibliography' | 'derivation';
149
+ type?: 'text' | 'textarea' | 'date' | 'number' | 'select' | 'multiselect' | 'reference' | 'bibliography' | 'derivation';
150
150
  options?: readonly {
151
151
  value: string;
152
152
  label: string;
@@ -212,6 +212,12 @@ export interface EditorFeature<C = unknown> {
212
212
  toolbarGroups?: ToolbarGroupDef[];
213
213
  /** Rules a command's `creates` must pass at the caret besides the schema (e.g. a Baustein's accepted children). */
214
214
  insertGuards?: (ctx: FeatureContext) => InsertGuard[];
215
+ /**
216
+ * Refusals of whole transactions: every transaction that changes the document runs through the guards of all
217
+ * features, in feature order; the first one answering a string refuses it and shows that string as a notice. For rules
218
+ * about the document as a whole that no schema expresses (a locked paragraph, a required document shape).
219
+ */
220
+ transactionGuards?: (ctx: FeatureContext) => TransactionGuard[];
215
221
  /** Components mounted beside the editor content; they receive `{ editor }`. `C` is the component type of the framework layer that renders them. */
216
222
  overlays?: readonly C[];
217
223
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"feature.js","names":[],"sources":["../../src/core/feature.ts"],"sourcesContent":["import type { HocuspocusProvider } from '@hocuspocus/provider'\nimport type { AnyExtension, Editor, JSONContent } from '@tiptap/core'\nimport type { Node as PMNode, ResolvedPos } from '@tiptap/pm/model'\nimport type { EditorState, Transaction } from '@tiptap/pm/state'\nimport type * as Y from 'yjs'\nimport type { Locale, Translate } from '../i18n'\nimport type { EditorSettings } from '../settings'\nimport type { EditorBackend } from './backend'\nimport type { BackendCapability } from './backend-capability'\nimport type { EditorAiConfig, EditorContext } from './context'\nimport type { ConvertTarget } from './convert'\nimport type { EditorUi } from './editor-ui'\nimport type { AbilitiesOptions, BlockAction } from './extension-points'\nimport type { MetadataRegistry } from './metadata'\nimport type { CommandDef, InsertGuard, ToolbarGroupDef } from './command-types'\nimport type { Notifier } from './notify'\nimport type { EditorMetadata } from './metadata-types'\nimport type { Store } from './store'\n\n// The composition contracts: an `EditorFeature` contributes extensions and descriptors, `defineEditor` validates a\n// feature list into an immutable `EditorPreset`, and `EditorSurface` mounts one. Everything here is inert data and\n// types; features only differ by which of these seams they fill.\n\n/**\n * The user writing in an editor: the collaboration session's `user`, labelled on remote carets, and (authors and comments\n * features) copied into the author mark of every text run and into every comment the user writes.\n */\nexport interface AuthorIdentity {\n id: string\n name: string\n color: string\n}\n\n/** A host-owned collaboration session handed to the surface; only the collab feature turns it into extensions. */\nexport interface CollaborationSession {\n ydoc: Y.Doc\n provider?: HocuspocusProvider\n user?: AuthorIdentity\n /**\n * Token the collaboration server's `Identity.authenticate` receives (`{ headers, token }`), for clients without\n * the session cookie of the server's origin (scripts, agents, cross-origin hosts). A string, or a function called\n * every time the socket opens (so it can refresh an expired token). Applied to `provider`'s configuration when the\n * surface is set up, which the provider reads when its socket opens: create the provider and mount the surface in the\n * same tick, or give the provider its own `token` option instead.\n */\n token?: string | (() => string) | (() => Promise<string>)\n}\n\n/**\n * A mapping between document positions and positions that survive collaborative edits. The collab feature supplies\n * the Yjs implementation; without one, `DocAnchors` maps positions through local transactions only.\n */\nexport interface AnchorResolver {\n /** The transaction applies a Yjs change to the document: remote edits and Yjs undo/redo. */\n isYSync: (tr: Transaction) => boolean\n /** The transaction applies an edit of another client (excludes Yjs undo/redo). */\n isRemoteChange: (tr: Transaction) => boolean\n /** Opaque token of `range` that `resolve` can turn back into positions after remote changes; null when unavailable. */\n capture: (state: EditorState, range: { from: number, to: number }, startInside: boolean) => unknown | null\n resolve: (state: EditorState, token: unknown) => { from: number, to: number } | null\n /** Stops undo capturing around a programmatic change; true iff a Yjs undo manager exists. */\n stopUndoCapture: (state: EditorState) => boolean\n}\n\n/** Per-surface dependencies of a feature's factories. Created when a surface is set up; never shared between editors. */\nexport interface FeatureContext {\n /** The app context the surface belongs to (shared by all editors of the app): app-level models key on it. */\n readonly app: EditorContext\n /** The app's shared settings (see `createEditorContext`): read with `get()`, write with `update()`, observe with `subscribe()`. */\n readonly settings: Store<EditorSettings>\n readonly metadata: MetadataRegistry\n /** Chrome of the editor this context belongs to (settings, sidebar). */\n readonly ui: EditorUi\n /** Shows a notice through the surface's framework (Nuxt UI toast, sonner). */\n readonly notify: Notifier\n /** The app's UI language (`createEditorContext({ locale })`). */\n readonly locale: Locale\n /** Renders a catalog message in the app's language; every label a feature contributes goes through it. */\n readonly t: Translate\n /** The data layer of the app (`createEditorContext({ backend })`): uploads, mention lookups, themes, AI. */\n readonly backend: EditorBackend\n /** Inert public AI configuration of the app context; absent when none was supplied. */\n readonly ai: EditorAiConfig | undefined\n /** Document whose package stores uploads (media feature). */\n readonly documentId: string | undefined\n readonly collaboration: CollaborationSession | undefined\n /** The collaboration-aware position mapping of the preset's `anchorResolver` feature; null means local mapping only. */\n readonly anchorResolver: AnchorResolver | null\n /** Host options for the ability extension (`abilities` prop). */\n readonly abilities: Partial<AbilitiesOptions>\n /** The final command list: the features' commands after the host's `commands` transformer. The `/` menu lists the ones with a `slash` placement. */\n readonly commands: () => CommandDef[]\n /** Undefined while extensions are constructed; set once the editor exists. Use it in event handlers. */\n getEditor: () => Editor | undefined\n}\n\n/** A tab of the settings panel/modal, contributed by the feature that owns those settings. */\nexport interface SettingsTab<C = unknown> {\n id: string\n /** Already translated (`ctx.t(...)` in the feature's `settingsTabs` factory). */\n label: string\n icon?: string\n component: C\n /** Props handed to `component`, e.g. the configured composite kinds. */\n props?: Record<string, unknown>\n}\n\n/** The stages `parseBlocks` and the import/AI insertion paths run; features replace stages, defaults are identity/Markdown. */\nexport interface BlockPipeline {\n parseMarkdown: (editor: Editor, markdown: string) => JSONContent[]\n normalize: (editor: Editor, blocks: JSONContent[]) => JSONContent[]\n segment: (editor: Editor, blocks: JSONContent[]) => JSONContent[]\n structure: (editor: Editor, blocks: JSONContent[]) => JSONContent[]\n wrapImport: (editor: Editor, file: File, blocks: JSONContent[]) => JSONContent[]\n migrateContent: (content: JSONContent) => JSONContent\n}\n\n/** Pipeline hooks that allow a single contributor; `normalize` chains instead. */\nexport const SINGLETON_PIPELINE_HOOKS = ['parseMarkdown', 'segment', 'structure', 'wrapImport', 'migrateContent'] as const satisfies readonly (keyof BlockPipeline)[]\n\n/**\n * A feature's block conversions. Targets keep the inert `ConvertTarget` shapes; the provider that parses a target id\n * owns that target (labels, execution). `handlesSource` claims all conversions of source blocks it understands.\n */\nexport interface ConversionProvider {\n /** Source blocks this provider converts to any target it can; the first provider claiming a node executes. */\n handlesSource?: (node: PMNode) => boolean\n /** Targets offered for `node` in menus (excluding its current type). */\n targets: (editor: Editor, node: PMNode) => ConvertTarget[]\n /** The target of `id` when this provider owns it, else null. */\n parseTarget: (id: string) => ConvertTarget | null\n /** Menu label (in the language of `t`) and icon of a target this provider owns. */\n label: (target: ConvertTarget, t: Translate) => { label: string, icon: string } | null\n /** The conversion of the block after `start` into `target` (absolute range replaced and the converted blocks), or null. */\n convert: (state: EditorState, start: ResolvedPos, target: ConvertTarget) => { from: number, to: number, blocks: PMNode[] } | null\n}\n\n/**\n * Inert public data AI reads about the editor's document model. A kind's `fields` carry what the model needs to write\n * them (`type`, and the choices of a `select`).\n */\nexport interface AiContextKind {\n id: string\n label: string\n fields: readonly { key: string, label: string, type?: 'text' | 'textarea' | 'date' | 'number' | 'select' | 'reference' | 'bibliography' | 'derivation', options?: readonly { value: string, label: string }[] }[]\n /** The kind's `ai.purpose`, when configured. */\n purpose?: string\n}\n\n/** A host block as AI learns it: what it is for, its Markdown form and one example. `id` is the node type name and the `convert` target id. */\nexport interface AiBlock {\n id: string\n label: string\n /** One or two German sentences: purpose and what it may contain. */\n description: string\n /** The Markdown form, placeholders in angle brackets, e.g. `!!! <info|warning|success>\\n<Inhalt>`. */\n syntax: string\n /** One complete Markdown example of the block. */\n example: string\n}\n\nexport interface AiContext {\n kinds?: readonly AiContextKind[]\n blocks?: readonly AiBlock[]\n structure?: 'flat' | 'composite'\n}\n\n/** One piece of material a feature fetched for a block before the model fills it (a host's examples, a record). */\nexport interface AiBlockDatum { label: string, text: string }\nexport interface AiBlockDataRequest { editor: Editor, node: PMNode, pos: number, signal: AbortSignal }\n/** Material for one block; `signal` aborts when the editor is destroyed or the run is cancelled. */\nexport type AiBlockDataProvider = (request: AiBlockDataRequest) => Promise<readonly AiBlockDatum[]> | readonly AiBlockDatum[]\n\n/**\n * The axis a feature belongs to: `content` (nodes, marks and structure rules the document stores: the server schema comes\n * from here), `interaction` (editing UX, backend-bound UI such as comments and collaboration) or `ai`. A feature\n * belongs to the dimension without which it makes no sense.\n */\nexport type Dimension = 'content' | 'interaction' | 'ai'\n\n/** One named contribution to an editor (blocks, toolbar entries, menus, settings, AI knowledge). Every member is optional but `dimension`; identity and ordering come from the feature list. */\nexport interface EditorFeature<C = unknown> {\n id: string\n /** The feature's primary dimension. */\n dimension: Dimension\n /**\n * The Vue-free schema-bearing extensions of this feature (no node views, no UI), collected by `schemaExtensionsOf` for\n * the server schema. Present on every feature whose nodes or marks the stored document can contain.\n */\n schemaExtensions?: () => AnyExtension[]\n /** Ids of features that must also be installed (`defineEditor` does not install them). */\n requires?: readonly string[]\n /** Backend capabilities the feature cannot work without; a surface session over a backend lacking one throws. Optional capabilities (users and tags for mentions) are not listed. */\n backend?: readonly BackendCapability[]\n extensions?: (ctx: FeatureContext) => AnyExtension[]\n blockActions?: (ctx: FeatureContext) => BlockAction[]\n /** The feature's commands, each declared once with its toolbar and `/` placements (see `CommandDef`). */\n commands?: (ctx: FeatureContext) => CommandDef<C>[]\n /** Toolbar groups the feature declares; every command's `toolbar.group` must be declared by some feature. Ids are unique across features. */\n toolbarGroups?: ToolbarGroupDef[]\n /** Rules a command's `creates` must pass at the caret besides the schema (e.g. a Baustein's accepted children). */\n insertGuards?: (ctx: FeatureContext) => InsertGuard[]\n /** Components mounted beside the editor content; they receive `{ editor }`. `C` is the component type of the framework layer that renders them. */\n overlays?: readonly C[]\n /**\n * Runs once when a surface mounts (per-app side effects, e.g. a theme runtime; dedupe inside). Returns the teardown,\n * called when the surface unmounts; it must be idempotent (React StrictMode mounts twice).\n */\n setup?: (ctx: FeatureContext) => void | (() => void)\n /** The feature's settings tabs. A factory over the app context, so labels render in the context's language. */\n settingsTabs?: (ctx: EditorContext) => readonly SettingsTab<C>[]\n /** Built-in metadata lists (`mentions`, `hashtags`, `speakers`, …) used for every list name the host does not supply; searched only when a suggestion uses them. */\n metadataDefaults?: (ctx: FeatureContext) => EditorMetadata\n blocks?: Partial<BlockPipeline>\n conversions?: readonly ConversionProvider[]\n /** At most one contributor: supplies the collaboration-aware position mapping to `DocAnchors`. */\n anchorResolver?: (ctx: FeatureContext) => AnchorResolver\n /** Additive: the contributions of all features are merged (`kinds` and `blocks` concatenated in feature order; `structure` may be set by one feature only). Reads AI's view of the document model. */\n aiContext?: (editor: Editor) => AiContext\n /** Additive: every contributor runs for the block (in parallel), their data concatenate in feature order; one rejection fails the whole fill. Read by „Baustein füllen (KI)“. */\n aiBlockData?: AiBlockDataProvider\n /** At most one contributor: formats an AI result's markdown before the generic pipeline. */\n formatAiResult?: (editor: Editor, markdown: string, heading?: string) => string\n}\n\n/** An immutable, validated feature list with its resolved pipeline, conversion providers and AI hooks. */\nexport interface EditorPreset<C = unknown> {\n readonly features: readonly EditorFeature<C>[]\n readonly pipeline: BlockPipeline\n readonly conversions: readonly ConversionProvider[]\n /** All `aiContext` contributions merged (`aiContextOf`). */\n readonly aiContext?: EditorFeature['aiContext']\n /** All `aiBlockData` contributions merged (`aiBlockDataOf`). */\n readonly aiBlockData?: AiBlockDataProvider\n /** The single `formatAiResult` contributor, if any. */\n readonly formatAiResult?: EditorFeature['formatAiResult']\n}\n"],"mappings":";;AAsHA,MAAa,2BAA2B;CAAC;CAAiB;CAAW;CAAa;CAAc;AAAgB"}
1
+ {"version":3,"file":"feature.js","names":[],"sources":["../../src/core/feature.ts"],"sourcesContent":["import type { HocuspocusProvider } from '@hocuspocus/provider'\nimport type { AnyExtension, Editor, JSONContent } from '@tiptap/core'\nimport type { Node as PMNode, ResolvedPos } from '@tiptap/pm/model'\nimport type { EditorState, Transaction } from '@tiptap/pm/state'\nimport type * as Y from 'yjs'\nimport type { Locale, Translate } from '../i18n'\nimport type { EditorSettings } from '../settings'\nimport type { EditorBackend } from './backend'\nimport type { BackendCapability } from './backend-capability'\nimport type { EditorAiConfig, EditorContext } from './context'\nimport type { ConvertTarget } from './convert'\nimport type { EditorUi } from './editor-ui'\nimport type { AbilitiesOptions, BlockAction } from './extension-points'\nimport type { MetadataRegistry } from './metadata'\nimport type { CommandDef, InsertGuard, ToolbarGroupDef, TransactionGuard } from './command-types'\nimport type { Notifier } from './notify'\nimport type { EditorMetadata } from './metadata-types'\nimport type { Store } from './store'\n\n// The composition contracts: an `EditorFeature` contributes extensions and descriptors, `defineEditor` validates a\n// feature list into an immutable `EditorPreset`, and `EditorSurface` mounts one. Everything here is inert data and\n// types; features only differ by which of these seams they fill.\n\n/**\n * The user writing in an editor: the collaboration session's `user`, labelled on remote carets, and (authors and comments\n * features) copied into the author mark of every text run and into every comment the user writes.\n */\nexport interface AuthorIdentity {\n id: string\n name: string\n color: string\n}\n\n/** A host-owned collaboration session handed to the surface; only the collab feature turns it into extensions. */\nexport interface CollaborationSession {\n ydoc: Y.Doc\n provider?: HocuspocusProvider\n user?: AuthorIdentity\n /**\n * Token the collaboration server's `Identity.authenticate` receives (`{ headers, token }`), for clients without\n * the session cookie of the server's origin (scripts, agents, cross-origin hosts). A string, or a function called\n * every time the socket opens (so it can refresh an expired token). Applied to `provider`'s configuration when the\n * surface is set up, which the provider reads when its socket opens: create the provider and mount the surface in the\n * same tick, or give the provider its own `token` option instead.\n */\n token?: string | (() => string) | (() => Promise<string>)\n}\n\n/**\n * A mapping between document positions and positions that survive collaborative edits. The collab feature supplies\n * the Yjs implementation; without one, `DocAnchors` maps positions through local transactions only.\n */\nexport interface AnchorResolver {\n /** The transaction applies a Yjs change to the document: remote edits and Yjs undo/redo. */\n isYSync: (tr: Transaction) => boolean\n /** The transaction applies an edit of another client (excludes Yjs undo/redo). */\n isRemoteChange: (tr: Transaction) => boolean\n /** Opaque token of `range` that `resolve` can turn back into positions after remote changes; null when unavailable. */\n capture: (state: EditorState, range: { from: number, to: number }, startInside: boolean) => unknown | null\n resolve: (state: EditorState, token: unknown) => { from: number, to: number } | null\n /** Stops undo capturing around a programmatic change; true iff a Yjs undo manager exists. */\n stopUndoCapture: (state: EditorState) => boolean\n}\n\n/** Per-surface dependencies of a feature's factories. Created when a surface is set up; never shared between editors. */\nexport interface FeatureContext {\n /** The app context the surface belongs to (shared by all editors of the app): app-level models key on it. */\n readonly app: EditorContext\n /** The app's shared settings (see `createEditorContext`): read with `get()`, write with `update()`, observe with `subscribe()`. */\n readonly settings: Store<EditorSettings>\n readonly metadata: MetadataRegistry\n /** Chrome of the editor this context belongs to (settings, sidebar). */\n readonly ui: EditorUi\n /** Shows a notice through the surface's framework (Nuxt UI toast, sonner). */\n readonly notify: Notifier\n /** The app's UI language (`createEditorContext({ locale })`). */\n readonly locale: Locale\n /** Renders a catalog message in the app's language; every label a feature contributes goes through it. */\n readonly t: Translate\n /** The data layer of the app (`createEditorContext({ backend })`): uploads, mention lookups, themes, AI. */\n readonly backend: EditorBackend\n /** Inert public AI configuration of the app context; absent when none was supplied. */\n readonly ai: EditorAiConfig | undefined\n /** Document whose package stores uploads (media feature). */\n readonly documentId: string | undefined\n readonly collaboration: CollaborationSession | undefined\n /** The collaboration-aware position mapping of the preset's `anchorResolver` feature; null means local mapping only. */\n readonly anchorResolver: AnchorResolver | null\n /** Host options for the ability extension (`abilities` prop). */\n readonly abilities: Partial<AbilitiesOptions>\n /** The final command list: the features' commands after the host's `commands` transformer. The `/` menu lists the ones with a `slash` placement. */\n readonly commands: () => CommandDef[]\n /** Undefined while extensions are constructed; set once the editor exists. Use it in event handlers. */\n getEditor: () => Editor | undefined\n}\n\n/** A tab of the settings panel/modal, contributed by the feature that owns those settings. */\nexport interface SettingsTab<C = unknown> {\n id: string\n /** Already translated (`ctx.t(...)` in the feature's `settingsTabs` factory). */\n label: string\n icon?: string\n component: C\n /** Props handed to `component`, e.g. the configured composite kinds. */\n props?: Record<string, unknown>\n}\n\n/** The stages `parseBlocks` and the import/AI insertion paths run; features replace stages, defaults are identity/Markdown. */\nexport interface BlockPipeline {\n parseMarkdown: (editor: Editor, markdown: string) => JSONContent[]\n normalize: (editor: Editor, blocks: JSONContent[]) => JSONContent[]\n segment: (editor: Editor, blocks: JSONContent[]) => JSONContent[]\n structure: (editor: Editor, blocks: JSONContent[]) => JSONContent[]\n wrapImport: (editor: Editor, file: File, blocks: JSONContent[]) => JSONContent[]\n migrateContent: (content: JSONContent) => JSONContent\n}\n\n/** Pipeline hooks that allow a single contributor; `normalize` chains instead. */\nexport const SINGLETON_PIPELINE_HOOKS = ['parseMarkdown', 'segment', 'structure', 'wrapImport', 'migrateContent'] as const satisfies readonly (keyof BlockPipeline)[]\n\n/**\n * A feature's block conversions. Targets keep the inert `ConvertTarget` shapes; the provider that parses a target id\n * owns that target (labels, execution). `handlesSource` claims all conversions of source blocks it understands.\n */\nexport interface ConversionProvider {\n /** Source blocks this provider converts to any target it can; the first provider claiming a node executes. */\n handlesSource?: (node: PMNode) => boolean\n /** Targets offered for `node` in menus (excluding its current type). */\n targets: (editor: Editor, node: PMNode) => ConvertTarget[]\n /** The target of `id` when this provider owns it, else null. */\n parseTarget: (id: string) => ConvertTarget | null\n /** Menu label (in the language of `t`) and icon of a target this provider owns. */\n label: (target: ConvertTarget, t: Translate) => { label: string, icon: string } | null\n /** The conversion of the block after `start` into `target` (absolute range replaced and the converted blocks), or null. */\n convert: (state: EditorState, start: ResolvedPos, target: ConvertTarget) => { from: number, to: number, blocks: PMNode[] } | null\n}\n\n/**\n * Inert public data AI reads about the editor's document model. A kind's `fields` carry what the model needs to write\n * them (`type`, and the choices of a `select`).\n */\nexport interface AiContextKind {\n id: string\n label: string\n fields: readonly { key: string, label: string, type?: 'text' | 'textarea' | 'date' | 'number' | 'select' | 'multiselect' | 'reference' | 'bibliography' | 'derivation', options?: readonly { value: string, label: string }[] }[]\n /** The kind's `ai.purpose`, when configured. */\n purpose?: string\n}\n\n/** A host block as AI learns it: what it is for, its Markdown form and one example. `id` is the node type name and the `convert` target id. */\nexport interface AiBlock {\n id: string\n label: string\n /** One or two German sentences: purpose and what it may contain. */\n description: string\n /** The Markdown form, placeholders in angle brackets, e.g. `!!! <info|warning|success>\\n<Inhalt>`. */\n syntax: string\n /** One complete Markdown example of the block. */\n example: string\n}\n\nexport interface AiContext {\n kinds?: readonly AiContextKind[]\n blocks?: readonly AiBlock[]\n structure?: 'flat' | 'composite'\n}\n\n/** One piece of material a feature fetched for a block before the model fills it (a host's examples, a record). */\nexport interface AiBlockDatum { label: string, text: string }\nexport interface AiBlockDataRequest { editor: Editor, node: PMNode, pos: number, signal: AbortSignal }\n/** Material for one block; `signal` aborts when the editor is destroyed or the run is cancelled. */\nexport type AiBlockDataProvider = (request: AiBlockDataRequest) => Promise<readonly AiBlockDatum[]> | readonly AiBlockDatum[]\n\n/**\n * The axis a feature belongs to: `content` (nodes, marks and structure rules the document stores: the server schema comes\n * from here), `interaction` (editing UX, backend-bound UI such as comments and collaboration) or `ai`. A feature\n * belongs to the dimension without which it makes no sense.\n */\nexport type Dimension = 'content' | 'interaction' | 'ai'\n\n/** One named contribution to an editor (blocks, toolbar entries, menus, settings, AI knowledge). Every member is optional but `dimension`; identity and ordering come from the feature list. */\nexport interface EditorFeature<C = unknown> {\n id: string\n /** The feature's primary dimension. */\n dimension: Dimension\n /**\n * The Vue-free schema-bearing extensions of this feature (no node views, no UI), collected by `schemaExtensionsOf` for\n * the server schema. Present on every feature whose nodes or marks the stored document can contain.\n */\n schemaExtensions?: () => AnyExtension[]\n /** Ids of features that must also be installed (`defineEditor` does not install them). */\n requires?: readonly string[]\n /** Backend capabilities the feature cannot work without; a surface session over a backend lacking one throws. Optional capabilities (users and tags for mentions) are not listed. */\n backend?: readonly BackendCapability[]\n extensions?: (ctx: FeatureContext) => AnyExtension[]\n blockActions?: (ctx: FeatureContext) => BlockAction[]\n /** The feature's commands, each declared once with its toolbar and `/` placements (see `CommandDef`). */\n commands?: (ctx: FeatureContext) => CommandDef<C>[]\n /** Toolbar groups the feature declares; every command's `toolbar.group` must be declared by some feature. Ids are unique across features. */\n toolbarGroups?: ToolbarGroupDef[]\n /** Rules a command's `creates` must pass at the caret besides the schema (e.g. a Baustein's accepted children). */\n insertGuards?: (ctx: FeatureContext) => InsertGuard[]\n /**\n * Refusals of whole transactions: every transaction that changes the document runs through the guards of all\n * features, in feature order; the first one answering a string refuses it and shows that string as a notice. For rules\n * about the document as a whole that no schema expresses (a locked paragraph, a required document shape).\n */\n transactionGuards?: (ctx: FeatureContext) => TransactionGuard[]\n /** Components mounted beside the editor content; they receive `{ editor }`. `C` is the component type of the framework layer that renders them. */\n overlays?: readonly C[]\n /**\n * Runs once when a surface mounts (per-app side effects, e.g. a theme runtime; dedupe inside). Returns the teardown,\n * called when the surface unmounts; it must be idempotent (React StrictMode mounts twice).\n */\n setup?: (ctx: FeatureContext) => void | (() => void)\n /** The feature's settings tabs. A factory over the app context, so labels render in the context's language. */\n settingsTabs?: (ctx: EditorContext) => readonly SettingsTab<C>[]\n /** Built-in metadata lists (`mentions`, `hashtags`, `speakers`, …) used for every list name the host does not supply; searched only when a suggestion uses them. */\n metadataDefaults?: (ctx: FeatureContext) => EditorMetadata\n blocks?: Partial<BlockPipeline>\n conversions?: readonly ConversionProvider[]\n /** At most one contributor: supplies the collaboration-aware position mapping to `DocAnchors`. */\n anchorResolver?: (ctx: FeatureContext) => AnchorResolver\n /** Additive: the contributions of all features are merged (`kinds` and `blocks` concatenated in feature order; `structure` may be set by one feature only). Reads AI's view of the document model. */\n aiContext?: (editor: Editor) => AiContext\n /** Additive: every contributor runs for the block (in parallel), their data concatenate in feature order; one rejection fails the whole fill. Read by „Baustein füllen (KI)“. */\n aiBlockData?: AiBlockDataProvider\n /** At most one contributor: formats an AI result's markdown before the generic pipeline. */\n formatAiResult?: (editor: Editor, markdown: string, heading?: string) => string\n}\n\n/** An immutable, validated feature list with its resolved pipeline, conversion providers and AI hooks. */\nexport interface EditorPreset<C = unknown> {\n readonly features: readonly EditorFeature<C>[]\n readonly pipeline: BlockPipeline\n readonly conversions: readonly ConversionProvider[]\n /** All `aiContext` contributions merged (`aiContextOf`). */\n readonly aiContext?: EditorFeature['aiContext']\n /** All `aiBlockData` contributions merged (`aiBlockDataOf`). */\n readonly aiBlockData?: AiBlockDataProvider\n /** The single `formatAiResult` contributor, if any. */\n readonly formatAiResult?: EditorFeature['formatAiResult']\n}\n"],"mappings":";;AAsHA,MAAa,2BAA2B;CAAC;CAAiB;CAAW;CAAa;CAAc;AAAgB"}
@@ -5,7 +5,7 @@ import { AiStreamEvent, BackendCommentsOptions, BackendError, CollaborationToken
5
5
  import { Notice, Notifier, silentNotifier } from "./notify.js";
6
6
  import { EditorAiConfig, EditorContext, EditorContextOptions, createEditorContext } from "./context.js";
7
7
  import { CORE_TARGET_IDS, ConvertTarget, Converter, ForeignBlocks, TextblockSpec, childrenOf, convertCoreBlocks, convertTargetId, coreConversionTargets, createConverter, finishConversion, isCoreSource, isCoreTarget, isCurrentTarget, isListNode, isQuoteNode, isTextblockNode, isWrapperBlock, parseCoreTarget, textblockSpecOf } from "./convert.js";
8
- import { CommandBase, CommandDef, CommandState, Creates, InsertGuard, MenuItem, PlacedToolbarGroup, ToolbarGroupDef } from "./command-types.js";
8
+ import { CommandBase, CommandDef, CommandState, Creates, InsertGuard, MenuItem, PlacedToolbarGroup, ToolbarGroupDef, TransactionGuard } from "./command-types.js";
9
9
  import { AbilitiesOptions, BlockAction, BlockActionContext, BlockTarget } from "./extension-points.js";
10
10
  import { EditorEntry, EditorUi, editorEntryOf, notifierOf, registerEditor } from "./editor-ui.js";
11
11
  import { EditorMetadata, METADATA_LIST, MetadataItem, MetadataQuery, MetadataSource } from "./metadata-types.js";
@@ -17,6 +17,7 @@ import { startBlockDrag } from "./block-drag.js";
17
17
  import { BlockFlash } from "./block-flash.js";
18
18
  import { CoreUi, coreBlockActions, coreContentFeature, coreUiFeature } from "./core-feature.js";
19
19
  import { canCreateAt, command, commandState, commandsOf, guardsAccept, toolbarOf } from "./commands.js";
20
+ import { TransactionGuards, TransactionGuardsOptions, transactionGuardExtensions } from "./transaction-guards.js";
20
21
  import { contributions, defineEditor, settingsTabsOf } from "./define-editor.js";
21
22
  import { DocumentMarkdown } from "./document-markdown.js";
22
23
  import { downloadText } from "./download.js";
@@ -30,4 +31,4 @@ import { selectionRange } from "./selection-range.js";
30
31
  import { SlashCommand, SlashCommandOptions, SlashCommandPluginKey } from "./slash.js";
31
32
  import { SuggestionDisplay, SuggestionListProps, SuggestionPopup, SuggestionPopupHandle, SuggestionPopupRenderer, createSuggestionRender, suggestionPopupOf } from "./suggestion-popup.js";
32
33
  import { CORE_TOOLBAR_GROUPS, coreCommands } from "./toolbar-items.js";
33
- export { Abilities, type AbilitiesOptions, type AbilitiesStorage, type AiBlock, type AiBlockDataProvider, type AiBlockDataRequest, type AiBlockDatum, type AiContext, type AiContextKind, type AiStreamEvent, type AnchorResolver, type AuthorIdentity, type BackendCapability, type BackendCommentsOptions, BackendError, type BlockAction, type BlockActionContext, BlockConvert, BlockFlash, type BlockPipeline, type BlockTarget, CATALOGS, CORE_TARGET_IDS, CORE_TOOLBAR_GROUPS, type CollaborationSession, type CollaborationToken, type CommandBase, type CommandDef, type CommandState, type ConversionProvider, type ConvertTarget, type Converter, type CoreUi, type Creates, DEFAULT_ABILITY_RULES, DEFAULT_BLOCK_PIPELINE, DEFAULT_LOCALE, type Dimension, type DocAnchor, DocAnchors, type DocAnchorsMeta, type DocAnchorsOptions, DocumentMarkdown, type EditorAiConfig, type EditorBackend, type EditorContext, type EditorContextOptions, type EditorEntry, type EditorFeature, type EditorMetadata, type EditorPreset, type EditorUi, type FeatureContext, type FeatureContextOptions, type ForeignBlocks, type InsertGuard, LOCALES, type Locale, METADATA_LIST, type MenuItem, type MessageKey, type MessageParams, type Messages, Metadata, type MetadataItem, type MetadataQuery, type MetadataRegistry, type MetadataSource, type Notice, type Notifier, PALETTE_OTHER_GROUP, type PaletteEntry, type PaletteGroup, type PlacedToolbarGroup, type PresetStorage, type ReadonlyStore, SINGLETON_PIPELINE_HOOKS, type SettingsTab, SlashCommand, type SlashCommandOptions, SlashCommandPluginKey, type Store, type SuggestionDisplay, type SuggestionListProps, SuggestionPopup, type SuggestionPopupHandle, type SuggestionPopupRenderer, type TextblockSpec, type ToolbarGroupDef, type Translate, abilitiesAt, abilitiesAtSelection, aiBlockDataOf, aiContextOf, anchorResolverOf, blockActionsOf, blockConversion, canCreateAt, captureRange, childrenOf, command, commandState, commandsOf, contributions, conversionLabel, conversionTargetGroups, conversionsOf, convertCoreBlocks, convertMenuItems, convertTargetId, coreBlockActions, coreCommands, coreContentFeature, coreConversionTargets, coreConversions, coreUiFeature, createConverter, createEditorContext, createFeatureContext, createMetadataRegistry, createStore, createSuggestionRender, createTranslator, defineEditor, derived, docAnchorsKey, downloadText, editorEntryOf, fetchErrorMessage, filterMetadataItems, filterPalette, finishConversion, formatAiResultOf, guardsAccept, hasAbility, isCoreSource, isCoreTarget, isCurrentTarget, isListNode, isLocale, isQuoteNode, isTextblockNode, isWrapperBlock, metadataOf, notifierOf, paletteGroupsOf, parseBlocks, parseConversionTarget, parseCoreTarget, pipelineOf, presetExtension, registerEditor, releaseRanges, resolveRange, selectionRange, settingsTabsOf, sha256Hex, silentNotifier, startBlockDrag, structureBlocks, suggestionPopupOf, targetOwner, textblockSpecOf, toolbarOf, wrapImportBlocks };
34
+ export { Abilities, type AbilitiesOptions, type AbilitiesStorage, type AiBlock, type AiBlockDataProvider, type AiBlockDataRequest, type AiBlockDatum, type AiContext, type AiContextKind, type AiStreamEvent, type AnchorResolver, type AuthorIdentity, type BackendCapability, type BackendCommentsOptions, BackendError, type BlockAction, type BlockActionContext, BlockConvert, BlockFlash, type BlockPipeline, type BlockTarget, CATALOGS, CORE_TARGET_IDS, CORE_TOOLBAR_GROUPS, type CollaborationSession, type CollaborationToken, type CommandBase, type CommandDef, type CommandState, type ConversionProvider, type ConvertTarget, type Converter, type CoreUi, type Creates, DEFAULT_ABILITY_RULES, DEFAULT_BLOCK_PIPELINE, DEFAULT_LOCALE, type Dimension, type DocAnchor, DocAnchors, type DocAnchorsMeta, type DocAnchorsOptions, DocumentMarkdown, type EditorAiConfig, type EditorBackend, type EditorContext, type EditorContextOptions, type EditorEntry, type EditorFeature, type EditorMetadata, type EditorPreset, type EditorUi, type FeatureContext, type FeatureContextOptions, type ForeignBlocks, type InsertGuard, LOCALES, type Locale, METADATA_LIST, type MenuItem, type MessageKey, type MessageParams, type Messages, Metadata, type MetadataItem, type MetadataQuery, type MetadataRegistry, type MetadataSource, type Notice, type Notifier, PALETTE_OTHER_GROUP, type PaletteEntry, type PaletteGroup, type PlacedToolbarGroup, type PresetStorage, type ReadonlyStore, SINGLETON_PIPELINE_HOOKS, type SettingsTab, SlashCommand, type SlashCommandOptions, SlashCommandPluginKey, type Store, type SuggestionDisplay, type SuggestionListProps, SuggestionPopup, type SuggestionPopupHandle, type SuggestionPopupRenderer, type TextblockSpec, type ToolbarGroupDef, type TransactionGuard, TransactionGuards, type TransactionGuardsOptions, type Translate, abilitiesAt, abilitiesAtSelection, aiBlockDataOf, aiContextOf, anchorResolverOf, blockActionsOf, blockConversion, canCreateAt, captureRange, childrenOf, command, commandState, commandsOf, contributions, conversionLabel, conversionTargetGroups, conversionsOf, convertCoreBlocks, convertMenuItems, convertTargetId, coreBlockActions, coreCommands, coreContentFeature, coreConversionTargets, coreConversions, coreUiFeature, createConverter, createEditorContext, createFeatureContext, createMetadataRegistry, createStore, createSuggestionRender, createTranslator, defineEditor, derived, docAnchorsKey, downloadText, editorEntryOf, fetchErrorMessage, filterMetadataItems, filterPalette, finishConversion, formatAiResultOf, guardsAccept, hasAbility, isCoreSource, isCoreTarget, isCurrentTarget, isListNode, isLocale, isQuoteNode, isTextblockNode, isWrapperBlock, metadataOf, notifierOf, paletteGroupsOf, parseBlocks, parseConversionTarget, parseCoreTarget, pipelineOf, presetExtension, registerEditor, releaseRanges, resolveRange, selectionRange, settingsTabsOf, sha256Hex, silentNotifier, startBlockDrag, structureBlocks, suggestionPopupOf, targetOwner, textblockSpecOf, toolbarOf, transactionGuardExtensions, wrapImportBlocks };
@@ -20,6 +20,7 @@ import { SuggestionPopup, createSuggestionRender, suggestionPopupOf } from "./su
20
20
  import { SlashCommand, SlashCommandPluginKey } from "./slash.js";
21
21
  import { CORE_TOOLBAR_GROUPS, coreCommands } from "./toolbar-items.js";
22
22
  import { coreBlockActions, coreContentFeature, coreUiFeature } from "./core-feature.js";
23
+ import { TransactionGuards, transactionGuardExtensions } from "./transaction-guards.js";
23
24
  import { downloadText } from "./download.js";
24
25
  import { blockActionsOf, createFeatureContext } from "./feature-context.js";
25
26
  import { fetchErrorMessage } from "./fetch-error.js";
@@ -27,4 +28,4 @@ import { sha256Hex } from "./hash.js";
27
28
  import { METADATA_LIST } from "./metadata-types.js";
28
29
  import { PALETTE_OTHER_GROUP, filterPalette, paletteGroupsOf } from "./palette.js";
29
30
  import { selectionRange } from "./selection-range.js";
30
- export { Abilities, BackendError, BlockConvert, BlockFlash, CATALOGS, CORE_TARGET_IDS, CORE_TOOLBAR_GROUPS, DEFAULT_ABILITY_RULES, DEFAULT_BLOCK_PIPELINE, DEFAULT_LOCALE, DocAnchors, DocumentMarkdown, LOCALES, METADATA_LIST, Metadata, PALETTE_OTHER_GROUP, SINGLETON_PIPELINE_HOOKS, SlashCommand, SlashCommandPluginKey, SuggestionPopup, abilitiesAt, abilitiesAtSelection, aiBlockDataOf, aiContextOf, anchorResolverOf, blockActionsOf, blockConversion, canCreateAt, captureRange, childrenOf, command, commandState, commandsOf, contributions, conversionLabel, conversionTargetGroups, conversionsOf, convertCoreBlocks, convertMenuItems, convertTargetId, coreBlockActions, coreCommands, coreContentFeature, coreConversionTargets, coreConversions, coreUiFeature, createConverter, createEditorContext, createFeatureContext, createMetadataRegistry, createStore, createSuggestionRender, createTranslator, defineEditor, derived, docAnchorsKey, downloadText, editorEntryOf, fetchErrorMessage, filterMetadataItems, filterPalette, finishConversion, formatAiResultOf, guardsAccept, hasAbility, isCoreSource, isCoreTarget, isCurrentTarget, isListNode, isLocale, isQuoteNode, isTextblockNode, isWrapperBlock, metadataOf, notifierOf, paletteGroupsOf, parseBlocks, parseConversionTarget, parseCoreTarget, pipelineOf, presetExtension, registerEditor, releaseRanges, resolveRange, selectionRange, settingsTabsOf, sha256Hex, silentNotifier, startBlockDrag, structureBlocks, suggestionPopupOf, targetOwner, textblockSpecOf, toolbarOf, wrapImportBlocks };
31
+ export { Abilities, BackendError, BlockConvert, BlockFlash, CATALOGS, CORE_TARGET_IDS, CORE_TOOLBAR_GROUPS, DEFAULT_ABILITY_RULES, DEFAULT_BLOCK_PIPELINE, DEFAULT_LOCALE, DocAnchors, DocumentMarkdown, LOCALES, METADATA_LIST, Metadata, PALETTE_OTHER_GROUP, SINGLETON_PIPELINE_HOOKS, SlashCommand, SlashCommandPluginKey, SuggestionPopup, TransactionGuards, abilitiesAt, abilitiesAtSelection, aiBlockDataOf, aiContextOf, anchorResolverOf, blockActionsOf, blockConversion, canCreateAt, captureRange, childrenOf, command, commandState, commandsOf, contributions, conversionLabel, conversionTargetGroups, conversionsOf, convertCoreBlocks, convertMenuItems, convertTargetId, coreBlockActions, coreCommands, coreContentFeature, coreConversionTargets, coreConversions, coreUiFeature, createConverter, createEditorContext, createFeatureContext, createMetadataRegistry, createStore, createSuggestionRender, createTranslator, defineEditor, derived, docAnchorsKey, downloadText, editorEntryOf, fetchErrorMessage, filterMetadataItems, filterPalette, finishConversion, formatAiResultOf, guardsAccept, hasAbility, isCoreSource, isCoreTarget, isCurrentTarget, isListNode, isLocale, isQuoteNode, isTextblockNode, isWrapperBlock, metadataOf, notifierOf, paletteGroupsOf, parseBlocks, parseConversionTarget, parseCoreTarget, pipelineOf, presetExtension, registerEditor, releaseRanges, resolveRange, selectionRange, settingsTabsOf, sha256Hex, silentNotifier, startBlockDrag, structureBlocks, suggestionPopupOf, targetOwner, textblockSpecOf, toolbarOf, transactionGuardExtensions, wrapImportBlocks };
@@ -6,6 +6,7 @@ import { registerEditor } from "./editor-ui.js";
6
6
  import { toolbarOf } from "./commands.js";
7
7
  import { EMPTY_EDITOR_CONTENT } from "./schema/index.js";
8
8
  import { SuggestionPopup } from "./suggestion-popup.js";
9
+ import { transactionGuardExtensions } from "./transaction-guards.js";
9
10
  import { blockActionsOf, createFeatureContext } from "./feature-context.js";
10
11
  import { getSchema } from "@tiptap/core";
11
12
  //#region src/core/surface-session.ts
@@ -44,6 +45,7 @@ function createSurfaceSession(options) {
44
45
  presetExtension(preset),
45
46
  SuggestionPopup.configure({ renderer: options.suggestionRenderer }),
46
47
  ...contributions(preset, "extensions").flatMap((factory) => factory(ctx)),
48
+ ...transactionGuardExtensions(preset, ctx),
47
49
  ...options.extensions ?? []
48
50
  ];
49
51
  const schema = getSchema(extensions);
@@ -1 +1 @@
1
- {"version":3,"file":"surface-session.js","names":[],"sources":["../../src/core/surface-session.ts"],"sourcesContent":["import { type AnyExtension, type Editor, type EditorOptions, getSchema, type JSONContent } from '@tiptap/core'\nimport type { EditorContext } from './context'\nimport { contributions } from './define-editor'\nimport { type EditorUi, registerEditor } from './editor-ui'\nimport { type CommandDef, toolbarOf } from './commands'\nimport type { AbilitiesOptions, BlockAction } from './extension-points'\nimport type { CollaborationSession, EditorPreset } from './feature'\nimport { blockActionsOf, createFeatureContext } from './feature-context'\nimport { createMetadataRegistry } from './metadata'\nimport type { EditorMetadata, MetadataItem } from './metadata-types'\nimport type { Notifier } from './notify'\nimport { presetExtension } from './pipeline'\nimport { EMPTY_EDITOR_CONTENT } from './schema'\nimport { ROOT_ABILITIES } from './schema/abilities'\nimport { SuggestionPopup, type SuggestionPopupRenderer } from './suggestion-popup'\n\n/**\n * What a surface hands its session. The framework layer owns everything that lives in its reactivity: the chrome\n * (`ui`), the notice renderer, the suggestion popup renderer and the editor instance (`getEditor`).\n * `preset`, `collaboration`, `documentId`, `abilities`, `extensions`, `blockActions`, `commands`, `value` and\n * `editable` are read once, when the session is created; later changes reach it through `setValue`, `setEditable` and\n * `setMetadata`.\n */\nexport interface SurfaceSessionOptions<C = unknown> {\n app: EditorContext\n /** The features the editor is made of. With `collaboration` it must contain the collaboration feature (else creating the session throws). */\n preset: EditorPreset<C>\n /** The chrome the features reach through `FeatureContext.ui`; registered for the editor by `register`. */\n ui: EditorUi\n /** The surface's notice renderer (`FeatureContext.notify`). */\n notify: Notifier\n /** The framework's popup for `@` / `#` / `/` suggestions. */\n suggestionRenderer: SuggestionPopupRenderer\n /** The mounted editor, `undefined` until the framework layer created it; used by `FeatureContext.getEditor`, `setValue` and `setEditable`. */\n getEditor: () => Editor | undefined\n /** Collaborative mode: the Y.Doc is the document, so there is no `value` and no `onChange`. */\n collaboration?: CollaborationSession | undefined\n /** Standalone initial document (TipTap JSON, unproxied); absent = an empty document. */\n value?: JSONContent | undefined\n /** Default `true`. */\n editable?: boolean | undefined\n /** The host's lists; later ones go through `setMetadata`. */\n metadata?: EditorMetadata | undefined\n documentId?: string | undefined\n abilities?: Partial<AbilitiesOptions> | undefined\n commands?: ((defaults: CommandDef[]) => CommandDef[]) | undefined\n /** Added after the features' extensions. */\n extensions?: readonly AnyExtension[] | undefined\n /** Added after the features' block actions. */\n blockActions?: readonly BlockAction[] | undefined\n /** Standalone: the document after every edit (the JSON the session remembers as its own, see `setValue`). */\n onChange?: ((content: JSONContent) => void) | undefined\n /** A new item was created in a metadata list. */\n onMetadataCreate?: ((list: string, item: MetadataItem) => void) | undefined\n /** A document handed in (initially or through `setValue`) does not fit the schema. */\n onContentError?: ((error: Error) => void) | undefined\n /** A mounted editor was registered for the first time (see `register`). */\n onReady?: ((editor: Editor) => void) | undefined\n}\n\n/**\n * Everything of an `EditorSurface` that does not depend on the UI framework: the preset's extensions and context,\n * the document's validation and echo handling, the features' `setup` lifecycle and the registration of the editor's\n * chrome. The framework layer creates the session once (state of the component instance), mounts a TipTap editor from\n * `editorOptions`, and forwards prop changes and lifecycle moments to it.\n */\nexport interface SurfaceSession<C = unknown> {\n /**\n * Options for the TipTap editor. Standalone mode (no `collaboration`) carries the initial document (`content`, already\n * migrated; an empty document when the given one was rejected) and the `onUpdate` that feeds `onChange`. The framework\n * layer may add its own options (rendering flags) but must not replace these.\n */\n readonly editorOptions: Partial<EditorOptions> & { extensions: AnyExtension[] }\n /** The features' overlay components, in feature order; the framework layer renders them beside the editor with `{ editor }`. */\n readonly overlays: readonly C[]\n /**\n * Starts the features' `setup` contributions, then reports a rejected initial document through `onContentError` (once\n * per session, not per activation). A no-op while active. Pair every activation with `deactivate`: React StrictMode\n * mounts, cleans up and mounts again, which must leave exactly one active setup.\n */\n activate: () => void\n /** Runs the teardowns of the active `setup` contributions, in the order they were started; a no-op while inactive. */\n deactivate: () => void\n /**\n * Registers the chrome of `editor` (the raw instance, see `registerEditor`) and announces it through `onReady` the\n * first time this editor is registered, so a host toolbar that receives it finds the chrome. Returns the unregister\n * function (idempotent). Call it again after the framework layer replaced the editor instance.\n */\n register: (editor: Editor) => () => void\n /**\n * The host's document changed (standalone only; ignored with `collaboration`). Not an edit: no `onChange`, no update\n * event. Ignored when `value` is the very object `onChange` last received (the host handing the surface's own JSON\n * back), is absent, or was already given; otherwise it is migrated and checked against the schema and applied, or\n * reported through `onContentError` with the current document kept. A missing or destroyed editor ignores it.\n */\n setValue: (value: JSONContent | undefined) => void\n /**\n * Switches the editor's editable state; ignored when it equals the last given one, and while no (or a destroyed)\n * editor exists. TipTap's `setEditable(…, false)` changes the flag without any event, so it is followed by an empty\n * `addToHistory: false` transaction: components that follow `isEditable` on `transaction` (block UI, the composite\n * inspector) notice. It has no steps, so nothing syncs and the history is untouched.\n */\n setEditable: (editable: boolean) => void\n /** Replaces the host's metadata lists; ignored when `metadata` is the object that was last given. */\n setMetadata: (metadata: EditorMetadata | undefined) => void\n}\n\n/**\n * Builds the session of one mounted surface: metadata registry, feature context, the extension list, the block actions\n * and the initial document. Throws when `collaboration` is given and the preset has no collaboration feature. Extension\n * order is fixed: the preset extension, the suggestion popup, the features' extensions in feature order, the host's.\n * The document is checked as `preset.pipeline.migrateContent` followed by the schema's `check()`; a failure is an error\n * value, never a throw.\n */\nexport function createSurfaceSession<C = unknown>(options: SurfaceSessionOptions<C>): SurfaceSession<C> {\n const { app, preset, ui, collaboration, getEditor } = options\n if (collaboration && !preset.features.some(feature => feature.id === 'collab')) {\n throw new Error('@ecsplico/editor: `collaboration` needs a preset with the collaboration feature')\n }\n // The surface only consumes a `collaboration` session the host opened (`useEditorDocument(id, { collaboration })`\n // opens it through `backend.collaboration` and checks that itself), so the collaboration capability is never needed\n // here: a host with its own provider mounts the collaboration feature over a backend that offers none.\n for (const feature of preset.features) {\n for (const capability of feature.backend ?? []) {\n if (capability === 'collaboration') continue\n if (!app.backend[capability]) {\n throw new Error(`@ecsplico/editor: feature \"${feature.id}\" needs backend capability \"${capability}\", which \"${app.backend.name}\" lacks`)\n }\n }\n }\n\n const registry = createMetadataRegistry({ onCreate: (list, item) => options.onMetadataCreate?.(list, item) })\n registry.replace({ ...options.metadata })\n\n const ctx = createFeatureContext({\n app,\n preset,\n ui,\n metadata: registry,\n notify: options.notify,\n getEditor,\n ...(options.documentId !== undefined && { documentId: options.documentId }),\n ...(collaboration && { collaboration }),\n ...(options.abilities && { abilities: options.abilities }),\n ...(options.commands && { commands: options.commands }),\n })\n // Fails here, not on the first toolbar render, when a command names a toolbar group no feature declares.\n toolbarOf(preset, ctx)\n // A command whose ability the document root does not grant never shows (its state is permanently disabled): say so once.\n const root = new Set<string>(options.abilities?.root ?? ROOT_ABILITIES)\n for (const cmd of ctx.commands()) {\n if (cmd.ability && !root.has(cmd.ability)) {\n console.warn(`@ecsplico/editor: command \"${cmd.id}\" needs ability \"${cmd.ability}\", which neither ROOT_ABILITIES nor \\`abilities.root\\` grants; it stays unavailable at document root`)\n }\n }\n\n const extensions: AnyExtension[] = [\n presetExtension(preset),\n SuggestionPopup.configure({ renderer: options.suggestionRenderer }),\n ...contributions(preset, 'extensions').flatMap(factory => factory(ctx)),\n ...(options.extensions ?? []),\n ]\n const schema = getSchema(extensions)\n const overlays = preset.features.flatMap(feature => [...(feature.overlays ?? [])])\n const blockActions = blockActionsOf(preset, ctx, options.blockActions)\n\n const validated = (json: JSONContent): { json: JSONContent } | { error: Error } => {\n try {\n const migrated = preset.pipeline.migrateContent(json)\n schema.nodeFromJSON(migrated).check()\n return { json: migrated }\n }\n catch (error) {\n return { error: error instanceof Error ? error : new Error(String(error)) }\n }\n }\n\n // Standalone: the JSON last sent through `onChange`; the host handing it back is not an external change.\n let lastEmitted: JSONContent | undefined\n let pendingContentError: Error | undefined\n const editorOptions: SurfaceSession<C>['editorOptions'] = {\n extensions,\n editable: options.editable ?? true,\n editorProps: { attributes: { class: 'tiptap' } },\n }\n if (!collaboration) {\n const initial = options.value ? validated(options.value) : { json: EMPTY_EDITOR_CONTENT }\n if ('error' in initial) pendingContentError = initial.error\n editorOptions.content = 'json' in initial ? initial.json : EMPTY_EDITOR_CONTENT\n editorOptions.onUpdate = ({ editor }) => {\n lastEmitted = editor.getJSON()\n options.onChange?.(lastEmitted)\n }\n }\n\n // What the setters below already applied; they only react to a change.\n let appliedValue = options.value\n let appliedEditable = options.editable ?? true\n let appliedMetadata = options.metadata\n // Teardowns of the `setup` contributions that are currently active; undefined while inactive.\n let teardowns: (() => void)[] | undefined\n let announced: Editor | undefined\n\n return {\n editorOptions,\n overlays,\n activate() {\n if (teardowns) return\n const active: (() => void)[] = []\n teardowns = active\n for (const run of contributions(preset, 'setup')) {\n const teardown = run(ctx)\n if (teardown) active.push(teardown)\n }\n if (pendingContentError) {\n const error = pendingContentError\n pendingContentError = undefined\n options.onContentError?.(error)\n }\n },\n deactivate() {\n const active = teardowns\n teardowns = undefined\n for (const teardown of active ?? []) teardown()\n },\n register(editor) {\n const unregister = registerEditor(editor, { ui, preset, ctx, blockActions })\n if (announced !== editor) {\n announced = editor\n options.onReady?.(editor)\n }\n return unregister\n },\n setValue(value) {\n if (value === appliedValue) return\n appliedValue = value\n const editor = getEditor()\n if (collaboration || !value || !editor || editor.isDestroyed || value === lastEmitted) return\n const checked = validated(value)\n if ('error' in checked) {\n options.onContentError?.(checked.error)\n return\n }\n editor.commands.setContent(checked.json, { emitUpdate: false })\n },\n setEditable(editable) {\n if (editable === appliedEditable) return\n appliedEditable = editable\n const editor = getEditor()\n if (!editor || editor.isDestroyed) return\n editor.setEditable(editable, false)\n editor.view.dispatch(editor.state.tr.setMeta('addToHistory', false))\n },\n setMetadata(metadata) {\n if (metadata === appliedMetadata) return\n appliedMetadata = metadata\n registry.replace({ ...metadata })\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"surface-session.js","names":[],"sources":["../../src/core/surface-session.ts"],"sourcesContent":["import { type AnyExtension, type Editor, type EditorOptions, getSchema, type JSONContent } from '@tiptap/core'\nimport type { EditorContext } from './context'\nimport { contributions } from './define-editor'\nimport { type EditorUi, registerEditor } from './editor-ui'\nimport { type CommandDef, toolbarOf } from './commands'\nimport type { AbilitiesOptions, BlockAction } from './extension-points'\nimport type { CollaborationSession, EditorPreset } from './feature'\nimport { blockActionsOf, createFeatureContext } from './feature-context'\nimport { createMetadataRegistry } from './metadata'\nimport type { EditorMetadata, MetadataItem } from './metadata-types'\nimport type { Notifier } from './notify'\nimport { presetExtension } from './pipeline'\nimport { EMPTY_EDITOR_CONTENT } from './schema'\nimport { ROOT_ABILITIES } from './schema/abilities'\nimport { SuggestionPopup, type SuggestionPopupRenderer } from './suggestion-popup'\nimport { transactionGuardExtensions } from './transaction-guards'\n\n/**\n * What a surface hands its session. The framework layer owns everything that lives in its reactivity: the chrome\n * (`ui`), the notice renderer, the suggestion popup renderer and the editor instance (`getEditor`).\n * `preset`, `collaboration`, `documentId`, `abilities`, `extensions`, `blockActions`, `commands`, `value` and\n * `editable` are read once, when the session is created; later changes reach it through `setValue`, `setEditable` and\n * `setMetadata`.\n */\nexport interface SurfaceSessionOptions<C = unknown> {\n app: EditorContext\n /** The features the editor is made of. With `collaboration` it must contain the collaboration feature (else creating the session throws). */\n preset: EditorPreset<C>\n /** The chrome the features reach through `FeatureContext.ui`; registered for the editor by `register`. */\n ui: EditorUi\n /** The surface's notice renderer (`FeatureContext.notify`). */\n notify: Notifier\n /** The framework's popup for `@` / `#` / `/` suggestions. */\n suggestionRenderer: SuggestionPopupRenderer\n /** The mounted editor, `undefined` until the framework layer created it; used by `FeatureContext.getEditor`, `setValue` and `setEditable`. */\n getEditor: () => Editor | undefined\n /** Collaborative mode: the Y.Doc is the document, so there is no `value` and no `onChange`. */\n collaboration?: CollaborationSession | undefined\n /** Standalone initial document (TipTap JSON, unproxied); absent = an empty document. */\n value?: JSONContent | undefined\n /** Default `true`. */\n editable?: boolean | undefined\n /** The host's lists; later ones go through `setMetadata`. */\n metadata?: EditorMetadata | undefined\n documentId?: string | undefined\n abilities?: Partial<AbilitiesOptions> | undefined\n commands?: ((defaults: CommandDef[]) => CommandDef[]) | undefined\n /** Added after the features' extensions. */\n extensions?: readonly AnyExtension[] | undefined\n /** Added after the features' block actions. */\n blockActions?: readonly BlockAction[] | undefined\n /** Standalone: the document after every edit (the JSON the session remembers as its own, see `setValue`). */\n onChange?: ((content: JSONContent) => void) | undefined\n /** A new item was created in a metadata list. */\n onMetadataCreate?: ((list: string, item: MetadataItem) => void) | undefined\n /** A document handed in (initially or through `setValue`) does not fit the schema. */\n onContentError?: ((error: Error) => void) | undefined\n /** A mounted editor was registered for the first time (see `register`). */\n onReady?: ((editor: Editor) => void) | undefined\n}\n\n/**\n * Everything of an `EditorSurface` that does not depend on the UI framework: the preset's extensions and context,\n * the document's validation and echo handling, the features' `setup` lifecycle and the registration of the editor's\n * chrome. The framework layer creates the session once (state of the component instance), mounts a TipTap editor from\n * `editorOptions`, and forwards prop changes and lifecycle moments to it.\n */\nexport interface SurfaceSession<C = unknown> {\n /**\n * Options for the TipTap editor. Standalone mode (no `collaboration`) carries the initial document (`content`, already\n * migrated; an empty document when the given one was rejected) and the `onUpdate` that feeds `onChange`. The framework\n * layer may add its own options (rendering flags) but must not replace these.\n */\n readonly editorOptions: Partial<EditorOptions> & { extensions: AnyExtension[] }\n /** The features' overlay components, in feature order; the framework layer renders them beside the editor with `{ editor }`. */\n readonly overlays: readonly C[]\n /**\n * Starts the features' `setup` contributions, then reports a rejected initial document through `onContentError` (once\n * per session, not per activation). A no-op while active. Pair every activation with `deactivate`: React StrictMode\n * mounts, cleans up and mounts again, which must leave exactly one active setup.\n */\n activate: () => void\n /** Runs the teardowns of the active `setup` contributions, in the order they were started; a no-op while inactive. */\n deactivate: () => void\n /**\n * Registers the chrome of `editor` (the raw instance, see `registerEditor`) and announces it through `onReady` the\n * first time this editor is registered, so a host toolbar that receives it finds the chrome. Returns the unregister\n * function (idempotent). Call it again after the framework layer replaced the editor instance.\n */\n register: (editor: Editor) => () => void\n /**\n * The host's document changed (standalone only; ignored with `collaboration`). Not an edit: no `onChange`, no update\n * event. Ignored when `value` is the very object `onChange` last received (the host handing the surface's own JSON\n * back), is absent, or was already given; otherwise it is migrated and checked against the schema and applied, or\n * reported through `onContentError` with the current document kept. A missing or destroyed editor ignores it.\n */\n setValue: (value: JSONContent | undefined) => void\n /**\n * Switches the editor's editable state; ignored when it equals the last given one, and while no (or a destroyed)\n * editor exists. TipTap's `setEditable(…, false)` changes the flag without any event, so it is followed by an empty\n * `addToHistory: false` transaction: components that follow `isEditable` on `transaction` (block UI, the composite\n * inspector) notice. It has no steps, so nothing syncs and the history is untouched.\n */\n setEditable: (editable: boolean) => void\n /** Replaces the host's metadata lists; ignored when `metadata` is the object that was last given. */\n setMetadata: (metadata: EditorMetadata | undefined) => void\n}\n\n/**\n * Builds the session of one mounted surface: metadata registry, feature context, the extension list, the block actions\n * and the initial document. Throws when `collaboration` is given and the preset has no collaboration feature. Extension\n * order is fixed: the preset extension, the suggestion popup, the features' extensions in feature order, the host's.\n * The document is checked as `preset.pipeline.migrateContent` followed by the schema's `check()`; a failure is an error\n * value, never a throw.\n */\nexport function createSurfaceSession<C = unknown>(options: SurfaceSessionOptions<C>): SurfaceSession<C> {\n const { app, preset, ui, collaboration, getEditor } = options\n if (collaboration && !preset.features.some(feature => feature.id === 'collab')) {\n throw new Error('@ecsplico/editor: `collaboration` needs a preset with the collaboration feature')\n }\n // The surface only consumes a `collaboration` session the host opened (`useEditorDocument(id, { collaboration })`\n // opens it through `backend.collaboration` and checks that itself), so the collaboration capability is never needed\n // here: a host with its own provider mounts the collaboration feature over a backend that offers none.\n for (const feature of preset.features) {\n for (const capability of feature.backend ?? []) {\n if (capability === 'collaboration') continue\n if (!app.backend[capability]) {\n throw new Error(`@ecsplico/editor: feature \"${feature.id}\" needs backend capability \"${capability}\", which \"${app.backend.name}\" lacks`)\n }\n }\n }\n\n const registry = createMetadataRegistry({ onCreate: (list, item) => options.onMetadataCreate?.(list, item) })\n registry.replace({ ...options.metadata })\n\n const ctx = createFeatureContext({\n app,\n preset,\n ui,\n metadata: registry,\n notify: options.notify,\n getEditor,\n ...(options.documentId !== undefined && { documentId: options.documentId }),\n ...(collaboration && { collaboration }),\n ...(options.abilities && { abilities: options.abilities }),\n ...(options.commands && { commands: options.commands }),\n })\n // Fails here, not on the first toolbar render, when a command names a toolbar group no feature declares.\n toolbarOf(preset, ctx)\n // A command whose ability the document root does not grant never shows (its state is permanently disabled): say so once.\n const root = new Set<string>(options.abilities?.root ?? ROOT_ABILITIES)\n for (const cmd of ctx.commands()) {\n if (cmd.ability && !root.has(cmd.ability)) {\n console.warn(`@ecsplico/editor: command \"${cmd.id}\" needs ability \"${cmd.ability}\", which neither ROOT_ABILITIES nor \\`abilities.root\\` grants; it stays unavailable at document root`)\n }\n }\n\n const extensions: AnyExtension[] = [\n presetExtension(preset),\n SuggestionPopup.configure({ renderer: options.suggestionRenderer }),\n ...contributions(preset, 'extensions').flatMap(factory => factory(ctx)),\n ...transactionGuardExtensions(preset, ctx),\n ...(options.extensions ?? []),\n ]\n const schema = getSchema(extensions)\n const overlays = preset.features.flatMap(feature => [...(feature.overlays ?? [])])\n const blockActions = blockActionsOf(preset, ctx, options.blockActions)\n\n const validated = (json: JSONContent): { json: JSONContent } | { error: Error } => {\n try {\n const migrated = preset.pipeline.migrateContent(json)\n schema.nodeFromJSON(migrated).check()\n return { json: migrated }\n }\n catch (error) {\n return { error: error instanceof Error ? error : new Error(String(error)) }\n }\n }\n\n // Standalone: the JSON last sent through `onChange`; the host handing it back is not an external change.\n let lastEmitted: JSONContent | undefined\n let pendingContentError: Error | undefined\n const editorOptions: SurfaceSession<C>['editorOptions'] = {\n extensions,\n editable: options.editable ?? true,\n editorProps: { attributes: { class: 'tiptap' } },\n }\n if (!collaboration) {\n const initial = options.value ? validated(options.value) : { json: EMPTY_EDITOR_CONTENT }\n if ('error' in initial) pendingContentError = initial.error\n editorOptions.content = 'json' in initial ? initial.json : EMPTY_EDITOR_CONTENT\n editorOptions.onUpdate = ({ editor }) => {\n lastEmitted = editor.getJSON()\n options.onChange?.(lastEmitted)\n }\n }\n\n // What the setters below already applied; they only react to a change.\n let appliedValue = options.value\n let appliedEditable = options.editable ?? true\n let appliedMetadata = options.metadata\n // Teardowns of the `setup` contributions that are currently active; undefined while inactive.\n let teardowns: (() => void)[] | undefined\n let announced: Editor | undefined\n\n return {\n editorOptions,\n overlays,\n activate() {\n if (teardowns) return\n const active: (() => void)[] = []\n teardowns = active\n for (const run of contributions(preset, 'setup')) {\n const teardown = run(ctx)\n if (teardown) active.push(teardown)\n }\n if (pendingContentError) {\n const error = pendingContentError\n pendingContentError = undefined\n options.onContentError?.(error)\n }\n },\n deactivate() {\n const active = teardowns\n teardowns = undefined\n for (const teardown of active ?? []) teardown()\n },\n register(editor) {\n const unregister = registerEditor(editor, { ui, preset, ctx, blockActions })\n if (announced !== editor) {\n announced = editor\n options.onReady?.(editor)\n }\n return unregister\n },\n setValue(value) {\n if (value === appliedValue) return\n appliedValue = value\n const editor = getEditor()\n if (collaboration || !value || !editor || editor.isDestroyed || value === lastEmitted) return\n const checked = validated(value)\n if ('error' in checked) {\n options.onContentError?.(checked.error)\n return\n }\n editor.commands.setContent(checked.json, { emitUpdate: false })\n },\n setEditable(editable) {\n if (editable === appliedEditable) return\n appliedEditable = editable\n const editor = getEditor()\n if (!editor || editor.isDestroyed) return\n editor.setEditable(editable, false)\n editor.view.dispatch(editor.state.tr.setMeta('addToHistory', false))\n },\n setMetadata(metadata) {\n if (metadata === appliedMetadata) return\n appliedMetadata = metadata\n registry.replace({ ...metadata })\n },\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { Notifier } from "./notify.js";
2
+ import { TransactionGuard } from "./command-types.js";
3
+ import { EditorPreset, FeatureContext } from "./feature.js";
4
+ import { AnyExtension, Extension } from "@tiptap/core";
5
+ //#region src/core/transaction-guards.d.ts
6
+ export interface TransactionGuardsOptions {
7
+ /** The guards, in the order they run (the order of the features that contributed them). */
8
+ guards: readonly TransactionGuard[];
9
+ /** Shows the notice of a refused transaction (default: dropped). `transactionGuardExtensions` passes the surface's `ctx.notify`. */
10
+ notify: Notifier;
11
+ }
12
+ /**
13
+ * Refuses a transaction that changes the document when a `TransactionGuard` answers a string: the guards run in order,
14
+ * the first refusal wins, its string is shown through `notify` and the whole transaction is dropped. Transactions that
15
+ * leave the document alone (selection, meta, focus) are never judged.
16
+ */
17
+ export declare const TransactionGuards: Extension<TransactionGuardsOptions, any>;
18
+ /** The extension that enforces the `transactionGuards` of `preset`'s features: none when no feature contributes a guard. */
19
+ export declare function transactionGuardExtensions<C>(preset: EditorPreset<C>, ctx: FeatureContext): AnyExtension[];
20
+ //#endregion
21
+ //# sourceMappingURL=transaction-guards.d.ts.map