mirror of https://github.com/halo-dev/halo
fix: resolve the issue of generated titles missing id (#4997)
#### What type of PR is this? /kind bug /area console /milestone 2.12.x #### What this PR does / why we need it: 在 #4975 中,使用 `decorations` 重写了 heading,但由于 `decorations` 只会给 nodeview 增加 id,因此会导致 render 后的 html 中不存在 id。 默认编辑器 heading 渲染时,自动增加 id。 #### How to test it? 使用默认编辑器编辑时,增加 Heading。保存后查看主题端对应的 heading 是否存在 id。 #### Which issue(s) this PR fixes: Fixes #4994 #### Does this PR introduce a user-facing change? ```release-note 解决渲染后的 Heading 中不存在 id 的问题。 ```pull/5007/head
parent
5b5fe71544
commit
1f5bef71ac
|
@ -1,4 +1,4 @@
|
|||
import type { Editor, Range } from "@/tiptap/vue-3";
|
||||
import { mergeAttributes, type Editor, type Range } from "@/tiptap/vue-3";
|
||||
import TiptapParagraph from "@/extensions/paragraph";
|
||||
import TiptapHeading from "@tiptap/extension-heading";
|
||||
import type { HeadingOptions } from "@tiptap/extension-heading";
|
||||
|
@ -15,8 +15,24 @@ import MdiFormatHeader6 from "~icons/mdi/format-header-6";
|
|||
import { markRaw } from "vue";
|
||||
import { i18n } from "@/locales";
|
||||
import type { ExtensionOptions } from "@/types";
|
||||
import { Decoration, DecorationSet, Plugin, PluginKey } from "@/tiptap";
|
||||
import { ExtensionHeading } from "..";
|
||||
import { generateAnchor } from "@/utils";
|
||||
|
||||
const Blockquote = TiptapHeading.extend<ExtensionOptions & HeadingOptions>({
|
||||
renderHTML({ node, HTMLAttributes }) {
|
||||
const hasLevel = this.options.levels.includes(node.attrs.level);
|
||||
const level = hasLevel ? node.attrs.level : this.options.levels[0];
|
||||
const id = generateAnchor(node.textContent);
|
||||
HTMLAttributes.id = id;
|
||||
|
||||
return [
|
||||
`h${level}`,
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
0,
|
||||
];
|
||||
},
|
||||
|
||||
addOptions() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
|
@ -265,6 +281,32 @@ const Blockquote = TiptapHeading.extend<ExtensionOptions & HeadingOptions>({
|
|||
addExtensions() {
|
||||
return [TiptapParagraph];
|
||||
},
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("generate-heading-id"),
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
const { doc } = state;
|
||||
const decorations: Decoration[] = [];
|
||||
doc.descendants((node, pos) => {
|
||||
if (node.type.name === ExtensionHeading.name) {
|
||||
const id = generateAnchor(node.textContent);
|
||||
if (node.attrs.id !== id) {
|
||||
decorations.push(
|
||||
Decoration.node(pos, pos + node.nodeSize, {
|
||||
id,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
return DecorationSet.create(doc, decorations);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
export default Blockquote;
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export * from "./delete-node";
|
||||
export * from "./anchor";
|
||||
|
|
|
@ -47,7 +47,6 @@ import {
|
|||
ToolbarItem,
|
||||
Plugin,
|
||||
PluginKey,
|
||||
Decoration,
|
||||
DecorationSet,
|
||||
} from "@halo-dev/richtext-editor";
|
||||
import {
|
||||
|
@ -90,7 +89,6 @@ import type { PluginModule } from "@halo-dev/console-shared";
|
|||
import { useDebounceFn, useLocalStorage } from "@vueuse/core";
|
||||
import { onBeforeUnmount } from "vue";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { generateAnchor } from "@/utils/anchor";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
@ -295,27 +293,17 @@ onMounted(() => {
|
|||
addProseMirrorPlugins() {
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("generate-heading-id"),
|
||||
key: new PluginKey("get-heading-id"),
|
||||
props: {
|
||||
decorations: (state) => {
|
||||
const headings: HeadingNode[] = [];
|
||||
const { doc } = state;
|
||||
const decorations: Decoration[] = [];
|
||||
doc.descendants((node, pos) => {
|
||||
doc.descendants((node) => {
|
||||
if (node.type.name === ExtensionHeading.name) {
|
||||
const id = generateAnchor(node.textContent);
|
||||
if (node.attrs.id !== id) {
|
||||
decorations.push(
|
||||
Decoration.node(pos, pos + node.nodeSize, {
|
||||
id,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
headings.push({
|
||||
level: node.attrs.level,
|
||||
text: node.textContent,
|
||||
id,
|
||||
id: node.attrs.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -323,7 +311,7 @@ onMounted(() => {
|
|||
if (!selectedHeadingNode.value) {
|
||||
selectedHeadingNode.value = headings[0];
|
||||
}
|
||||
return DecorationSet.create(doc, decorations);
|
||||
return DecorationSet.empty;
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { generateAnchor } from "../anchor";
|
||||
|
||||
describe("generateAnchor", () => {
|
||||
it("should handle basic text", () => {
|
||||
expect(generateAnchor("Hello World")).toBe("hello-world");
|
||||
});
|
||||
|
||||
it("should trim whitespace", () => {
|
||||
expect(generateAnchor(" Hello World ")).toBe("hello-world");
|
||||
});
|
||||
|
||||
it("should replace multiple spaces with a single dash", () => {
|
||||
expect(generateAnchor("Hello World")).toBe("hello-world");
|
||||
});
|
||||
|
||||
it("should handle Chinese characters", () => {
|
||||
expect(generateAnchor("你好")).toBe("%E4%BD%A0%E5%A5%BD");
|
||||
});
|
||||
|
||||
it("should handle special characters", () => {
|
||||
expect(generateAnchor("Hello@#World$")).toBe("hello%40%23world%24");
|
||||
});
|
||||
|
||||
it("should handle empty string", () => {
|
||||
expect(generateAnchor("")).toBe("");
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue