mirror of
https://github.com/docmost/docmost.git
synced 2026-05-17 23:14:07 +08:00
feat: enforce strict transclusion schema
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Top-level block node types allowed inside a `transclusionSource`.
|
||||
* Notably excludes:
|
||||
* - `transclusionSource` — sync blocks cannot wrap other sync blocks (sources are leaves).
|
||||
* - `transclusionReference` — sync blocks cannot transclude other sync blocks,
|
||||
* which keeps the transclusion graph acyclic and lets the renderer skip
|
||||
* cycle-aware traversal entirely.
|
||||
*
|
||||
* Also excludes child-only nodes (`listItem`, `tableRow`, `column`, etc.)
|
||||
* — they're already constrained by their parent containers.
|
||||
*/
|
||||
export const TRANSCLUSION_SOURCE_ALLOWED_NODE_TYPES = [
|
||||
'paragraph',
|
||||
'heading',
|
||||
'blockquote',
|
||||
'codeBlock',
|
||||
'horizontalRule',
|
||||
'bulletList',
|
||||
'orderedList',
|
||||
'taskList',
|
||||
'image',
|
||||
'video',
|
||||
'audio',
|
||||
'attachment',
|
||||
'callout',
|
||||
'details',
|
||||
'embed',
|
||||
'mathBlock',
|
||||
'table',
|
||||
'drawio',
|
||||
'excalidraw',
|
||||
'pdf',
|
||||
'subpages',
|
||||
'columns',
|
||||
'youtube',
|
||||
] as const;
|
||||
|
||||
export type TransclusionSourceAllowedNodeType =
|
||||
(typeof TRANSCLUSION_SOURCE_ALLOWED_NODE_TYPES)[number];
|
||||
|
||||
export const TRANSCLUSION_SOURCE_CONTENT_EXPRESSION = `(${TRANSCLUSION_SOURCE_ALLOWED_NODE_TYPES.join(' | ')})+`;
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./constants";
|
||||
export * from "./transclusion-source";
|
||||
export * from "./transclusion-reference";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { TRANSCLUSION_SOURCE_CONTENT_EXPRESSION } from "./constants";
|
||||
|
||||
export interface TransclusionSourceOptions {
|
||||
HTMLAttributes: Record<string, any>;
|
||||
@@ -34,7 +34,8 @@ export const TransclusionSource = Node.create<TransclusionSourceOptions>({
|
||||
},
|
||||
|
||||
group: "block",
|
||||
content: "block+",
|
||||
// Schema-enforced allow-list. Excludes `transclusionSource` (no nesting)
|
||||
content: TRANSCLUSION_SOURCE_CONTENT_EXPRESSION,
|
||||
defining: true,
|
||||
isolating: true,
|
||||
|
||||
@@ -130,30 +131,4 @@ export const TransclusionSource = Node.create<TransclusionSourceOptions>({
|
||||
this.editor.isInitialized = true;
|
||||
return ReactNodeViewRenderer(this.options.view);
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const typeName = this.name;
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey(`${typeName}-noNesting`),
|
||||
filterTransaction: (tr) => {
|
||||
if (!tr.docChanged) return true;
|
||||
let nested = false;
|
||||
tr.doc.descendants((node, pos) => {
|
||||
if (nested) return false;
|
||||
if (node.type.name !== typeName) return true;
|
||||
const $pos = tr.doc.resolve(pos);
|
||||
for (let depth = $pos.depth; depth > 0; depth -= 1) {
|
||||
if ($pos.node(depth).type.name === typeName) {
|
||||
nested = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return !nested;
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user