AcadeDoc/packages/editor-ext/src/lib/wikilink.ts
Corentin b802f1d647 feat(editor-ext): share wikilink and database-view node schemas
Add WikilinkNode and DatabaseView schema-only nodes to @docmost/editor-ext
and register them in the Hocuspocus server tiptapExtensions list.

Without the shared schema, jsonToNode on the server hit a RangeError for
those node types and stripUnknownNodes dropped them on every collab save,
so wikilinks disappeared on page reload and database-view embeds lost
their config and rendered as empty placeholders.
2026-05-11 12:28:12 +00:00

99 lines
2.6 KiB
TypeScript

import { Node } from "@tiptap/core";
export interface WikilinkAttrs {
pageId: string | null;
slugId?: string | null;
spaceSlug?: string | null;
title: string;
alias: string | null;
}
declare module "@tiptap/core" {
interface Commands<ReturnType> {
wikilink: {
insertWikilink: (attrs: WikilinkAttrs) => ReturnType;
};
}
}
/**
* Shared Wikilink node (schema only, no NodeView).
*
* Lives in @docmost/editor-ext so both the Hocuspocus server and the React
* client share the exact same schema. The client extends this node to add
* the React NodeView, input rule, and suggestion plugin.
*
* Without registering this node on the server, Hocuspocus' jsonToNode strips
* wikilink nodes on save (unknown node type).
*/
export const WikilinkNode = Node.create({
name: "wikilink",
group: "inline",
inline: true,
atom: true,
selectable: true,
addAttributes() {
return {
pageId: {
default: null,
parseHTML: (el: HTMLElement) => el.getAttribute("data-page-id"),
renderHTML: (attrs) =>
attrs.pageId ? { "data-page-id": attrs.pageId } : {},
},
slugId: {
default: null,
parseHTML: (el: HTMLElement) => el.getAttribute("data-slug-id"),
renderHTML: (attrs) =>
attrs.slugId ? { "data-slug-id": attrs.slugId } : {},
},
spaceSlug: {
default: null,
parseHTML: (el: HTMLElement) => el.getAttribute("data-space-slug"),
renderHTML: (attrs) =>
attrs.spaceSlug ? { "data-space-slug": attrs.spaceSlug } : {},
},
title: {
default: "",
parseHTML: (el: HTMLElement) =>
el.getAttribute("data-title") ?? el.textContent ?? "",
renderHTML: (attrs) => ({ "data-title": attrs.title }),
},
alias: {
default: null,
parseHTML: (el: HTMLElement) => el.getAttribute("data-alias"),
renderHTML: (attrs) =>
attrs.alias ? { "data-alias": attrs.alias } : {},
},
};
},
parseHTML() {
return [{ tag: "span[data-wikilink]" }];
},
renderHTML({ HTMLAttributes, node }) {
const display = node.attrs.alias ?? node.attrs.title ?? "?";
const isBroken = !node.attrs.pageId;
return [
"span",
{
"data-wikilink": "true",
...HTMLAttributes,
class: isBroken ? "wikilink wikilink--broken" : "wikilink",
},
`[[${display}]]`,
];
},
addCommands() {
return {
insertWikilink:
(attrs: WikilinkAttrs) =>
({ commands }) =>
commands.insertContent({ type: this.name, attrs }),
};
},
});
export default WikilinkNode;