memos/web/src/components/MemoEditor/Editor/extensions.ts
boojack 00225db922 refactor(web): share markdown element styles between viewer and editor
Extract the Tailwind classes for common markdown elements (paragraph,
blockquote, lists, inline code, link, hr, headings) into a single
markdownStyles.ts consumed by both the read-only MemoContent components
and the WYSIWYG editor, replacing the duplicated per-element strings and
the .memo-wysiwyg CSS block. Heading classes are precomputed per level so
the hot renderHTML path is a lookup, not a cn() merge.

Also require at least one character after `#` before opening the tag
suggestion menu so a bare `#` (or `# ` heading) no longer conflicts with
markdown headings.
2026-06-13 22:17:00 +08:00

53 lines
2.4 KiB
TypeScript

import { type AnyExtension, mergeAttributes } from "@tiptap/core";
import { Heading } from "@tiptap/extension-heading";
import { TaskItem, TaskList } from "@tiptap/extension-list";
import { Markdown } from "@tiptap/markdown";
import StarterKit from "@tiptap/starter-kit";
import { type HeadingLevel, headingClass, markdownStyles } from "@/lib/markdownStyles";
import { preservedExtensions } from "./PreservedBlock";
import { Tag } from "./Tag";
/**
* StarterKit's Heading is bundled and cannot vary classes by level via static
* HTMLAttributes, so we disable it (heading: false) and render headings here.
* renderHTML only affects the editable DOM — heading markdown serialization is
* unchanged — so the round-trip codec is unaffected.
*/
const StyledHeading = Heading.extend({
renderHTML({ node, HTMLAttributes }) {
const { levels } = this.options;
const level = (levels.includes(node.attrs.level) ? node.attrs.level : levels[0]) as HeadingLevel;
return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { class: headingClass(level) }), 0];
},
});
/**
* The canonical schema-relevant extension set, shared by the live editor and
* the headless markdown codec so that parse/serialize behavior is identical
* in both. UI-only extensions (Placeholder, suggestion popups) are added by
* the editor component on top of this list and must never affect the schema.
*/
export function buildExtensions(): AnyExtension[] {
return [
StarterKit.configure({
heading: false,
link: { openOnClick: false, HTMLAttributes: { class: markdownStyles.link } },
// Markdown has no underline syntax; keeping the extension would let
// Ctrl+U create marks that cannot serialize. Out of the schema entirely.
underline: false,
paragraph: { HTMLAttributes: { class: markdownStyles.paragraph } },
blockquote: { HTMLAttributes: { class: markdownStyles.blockquote } },
bulletList: { HTMLAttributes: { class: markdownStyles.bulletList } },
orderedList: { HTMLAttributes: { class: markdownStyles.orderedList } },
listItem: { HTMLAttributes: { class: markdownStyles.listItem } },
code: { HTMLAttributes: { class: markdownStyles.inlineCode } },
horizontalRule: { HTMLAttributes: { class: markdownStyles.horizontalRule } },
}),
StyledHeading.configure({ levels: [1, 2, 3, 4, 5, 6] }),
TaskList,
TaskItem.configure({ nested: true }),
Markdown,
...preservedExtensions,
Tag,
];
}