Files
edgeever/packages/shared/src/merge-divider.ts
T
edgeever-updater[bot] 05e53dcba3 chore: update EdgeEver from v2.7.0
EdgeEver-Upstream-Commit: 30fcb3fc65c419000a3e6f8cfb70eca47ae53914
2026-10-26 07:48:43 +00:00

97 lines
2.5 KiB
TypeScript

import { mergeAttributes, Node } from "@tiptap/core";
import { openBlockSource } from "./markdown-block-start";
import type { TiptapDoc, TiptapNode } from "./content";
/** TipTap / ProseMirror node type for note-merge section boundaries. */
export const MERGE_DIVIDER_NODE_TYPE = "edgeeverMergeDivider" as const;
/** Markdown marker that round-trips through `docToMarkdown` / `markdownToDoc`. */
export const MERGE_DIVIDER_MARKDOWN_MARKER = "<!-- edgeever:merge-divider -->";
const MERGE_DIVIDER_TOKENIZER =
/^<!--\s*edgeever:merge-divider\s*-->\s*(?:\n+---[ \t]*(?:\n+|$)|(?:\n+|$))/;
/**
* Semantic horizontal rule inserted between source notes during merge.
* Distinct from user-authored `---` (`horizontalRule`) so themes cannot
* collapse merge boundaries into decorative hairlines.
*/
export const MergeDivider = Node.create({
name: MERGE_DIVIDER_NODE_TYPE,
group: "block",
atom: true,
selectable: true,
draggable: true,
parseHTML() {
return [{ tag: "hr[data-edgeever-merge-divider]" }];
},
renderHTML({ HTMLAttributes }) {
return [
"hr",
mergeAttributes(HTMLAttributes, {
"data-edgeever-merge-divider": "true",
class: "edgeever-merge-divider",
}),
];
},
renderMarkdown() {
return `${MERGE_DIVIDER_MARKDOWN_MARKER}\n\n---`;
},
parseMarkdown(_token, helpers) {
return helpers.createNode(MERGE_DIVIDER_NODE_TYPE);
},
markdownTokenizer: {
name: MERGE_DIVIDER_NODE_TYPE,
level: "block",
start(src: string) {
return openBlockSource(src).indexOf("<!-- edgeever:merge-divider -->");
},
tokenize(src: string) {
const match = MERGE_DIVIDER_TOKENIZER.exec(src);
if (!match) {
return undefined;
}
return {
type: MERGE_DIVIDER_NODE_TYPE,
raw: match[0],
text: "",
};
},
},
});
export const createMergeDividerNode = (): TiptapNode => ({
type: MERGE_DIVIDER_NODE_TYPE,
});
/**
* Concatenate source TipTap documents with semantic merge dividers between them.
* Empty or whitespace-only source docs are skipped so merges do not stack bare rules.
*/
export const mergeMemoDocs = (docs: TiptapDoc[]): TiptapDoc => {
const content: TiptapNode[] = [];
for (const doc of docs) {
const section = Array.isArray(doc.content) ? doc.content : [];
if (section.length === 0) {
continue;
}
if (content.length > 0) {
content.push(createMergeDividerNode());
}
content.push(...section);
}
return {
type: "doc",
content: content.length > 0 ? content : [{ type: "paragraph" }],
};
};