tfrere's picture
tfrere HF Staff
refactor(backend/publisher): transformer pipeline + shared registry + shiki highlighting
7843436
Raw
History Blame Contribute Delete
6.52 kB
/**
* Server-side TipTap extensions for generateHTML().
*
* Mirrors the frontend extension list but without:
* - React NodeViews (not available server-side)
* - Collaboration / CollaborationCursor / CollaborationUndo
* - Placeholder, SlashCommands, ImageUpload, Comment (editor-only)
*/
import { Node, mergeAttributes, type Extensions } from "@tiptap/core";
import { SHARED_COMPONENT_DEFS, type SharedComponentDef } from "../shared/component-defs.js";
import StarterKit from "@tiptap/starter-kit";
import { CodeBlock } from "@tiptap/extension-code-block";
import Mathematics from "@tiptap/extension-mathematics";
import { Image } from "@tiptap/extension-image";
import { Table } from "@tiptap/extension-table";
import { TableRow } from "@tiptap/extension-table-row";
import { TableCell } from "@tiptap/extension-table-cell";
import { TableHeader } from "@tiptap/extension-table-header";
// ---- Custom inline nodes (server versions, no NodeView) ----
const CitationServer = Node.create({
name: "citation",
group: "inline",
inline: true,
atom: true,
addAttributes() {
return {
key: { default: "" },
label: { default: "" },
};
},
parseHTML() {
return [{ tag: 'span[data-type="citation"]' }];
},
renderHTML({ HTMLAttributes, node }) {
const label = node.attrs.label || `[${node.attrs.key}]`;
return [
"span",
mergeAttributes(HTMLAttributes, {
"data-type": "citation",
class: "citation-node",
}),
label,
];
},
});
const BibliographyServer = Node.create({
name: "bibliography",
group: "block",
atom: true,
addAttributes() {
return {
renderedHtml: { default: "" },
};
},
parseHTML() {
return [{ tag: 'div[data-type="bibliography"]' }];
},
renderHTML({ HTMLAttributes }) {
// Render an empty placeholder; postProcess in html-renderer will inject
// the actual bibliography HTML from the renderedhtml attribute.
return [
"div",
mergeAttributes(HTMLAttributes, {
"data-type": "bibliography",
class: "bibliography-block",
}),
];
},
});
const GlossaryServer = Node.create({
name: "glossary",
group: "inline",
inline: true,
atom: true,
addAttributes() {
return {
term: { default: "" },
definition: { default: "" },
};
},
parseHTML() {
return [{ tag: 'span[data-type="glossary"]' }];
},
renderHTML({ HTMLAttributes, node }) {
const term = (node.attrs.term as string) || "";
const definition = (node.attrs.definition as string) || "";
return [
"span",
mergeAttributes(HTMLAttributes, {
"data-type": "glossary",
class: "glossary-node",
tabindex: "0",
}),
term,
[
"span",
{ class: "pub-tooltip pub-tooltip--glossary", "aria-hidden": "true" },
["span", { class: "pub-tooltip__title" }, term],
["span", { class: "pub-tooltip__body" }, definition],
],
];
},
});
const FootnoteServer = Node.create({
name: "footnote",
group: "inline",
inline: true,
atom: true,
addAttributes() {
return {
content: { default: "" },
};
},
parseHTML() {
return [{ tag: 'span[data-type="footnote"]' }];
},
renderHTML({ HTMLAttributes, node }) {
return [
"span",
mergeAttributes(HTMLAttributes, {
"data-type": "footnote",
class: "footnote-node",
title: node.attrs.content,
tabindex: "0",
}),
["sup", { class: "footnote-marker" }, "*"],
];
},
});
// ---- Stack ----
const StackColumnServer = Node.create({
name: "stackColumn",
group: "stackColumn",
content: "block+",
defining: true,
isolating: true,
parseHTML() {
return [{ tag: 'div[data-type="stack-column"]' }];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(HTMLAttributes, { "data-type": "stack-column" }),
0,
];
},
});
const StackServer = Node.create({
name: "stack",
group: "block",
content: "stackColumn{2,4}",
defining: true,
isolating: true,
addAttributes() {
return {
layout: { default: "2-column" },
gap: { default: "medium" },
};
},
parseHTML() {
return [{ tag: 'div[data-component="stack"]' }];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(HTMLAttributes, { "data-component": "stack" }),
0,
];
},
});
// ---- Generic wrapper/atomic components from the shared registry ----
function makeServerWrapperExt(def: SharedComponentDef) {
return Node.create({
name: def.name,
group: "block",
content: def.content || "block+",
defining: true,
isolating: true,
addAttributes() {
const attrs: Record<string, { default: unknown }> = {};
for (const f of def.fields) attrs[f.name] = { default: f.default ?? null };
return attrs;
},
parseHTML() {
return [{ tag: `div[data-component="${def.name}"]` }];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(HTMLAttributes, { "data-component": def.name }),
0,
];
},
});
}
function makeServerAtomicExt(def: SharedComponentDef) {
return Node.create({
name: def.name,
group: "block",
atom: true,
addAttributes() {
const attrs: Record<string, { default: unknown }> = {};
for (const f of def.fields) attrs[f.name] = { default: f.default ?? null };
return attrs;
},
parseHTML() {
return [{ tag: `div[data-component="${def.name}"]` }];
},
renderHTML({ HTMLAttributes }) {
return [
"div",
mergeAttributes(HTMLAttributes, { "data-component": def.name }),
];
},
});
}
export function getServerExtensions(): Extensions {
const wrappers = SHARED_COMPONENT_DEFS.filter((d) => d.kind === "wrapper").map(makeServerWrapperExt);
const atomics = SHARED_COMPONENT_DEFS.filter((d) => d.kind === "atomic").map(makeServerAtomicExt);
return [
StarterKit.configure({
codeBlock: false,
undoRedo: false,
link: { openOnClick: false },
} as any),
CodeBlock,
Mathematics.configure({ katexOptions: { throwOnError: false } }),
Image.configure({ allowBase64: true }),
Table.configure({ resizable: false }),
TableRow,
TableCell,
TableHeader,
CitationServer,
BibliographyServer,
GlossaryServer,
FootnoteServer,
StackServer,
StackColumnServer,
...wrappers,
...atomics,
];
}