fix: remove trailing empty paragraphs after lists in rich text content

This commit is contained in:
JOYCEQL
2026-04-01 08:12:59 +08:00
parent 042bb723fe
commit 7111b3fd21
2 changed files with 30 additions and 6 deletions
@@ -35,7 +35,11 @@ import {
} from "lucide-react";
import Highlight from "@tiptap/extension-highlight";
import { cn } from "@/lib/utils";
import { normalizeLinkHref, stripLegacyRichTextClasses } from "@/lib/richText";
import {
normalizeLinkHref,
stripLegacyRichTextClasses,
stripTrailingListParagraph,
} from "@/lib/richText";
import { BetterSpace } from "./BetterSpace";
import { toast } from "sonner";
import "@/styles/tiptap.scss";
@@ -405,7 +409,7 @@ const RichTextEditor = ({
}: RichTextEditorProps) => {
const t = useTranslations("richEditor");
const initialContent = useMemo(
() => stripLegacyRichTextClasses(content),
() => stripTrailingListParagraph(stripLegacyRichTextClasses(content)),
[]
);
@@ -472,7 +476,9 @@ const RichTextEditor = ({
extensions,
content: initialContent,
onUpdate: ({ editor }) => {
onChange(stripLegacyRichTextClasses(editor.getHTML()));
onChange(
stripTrailingListParagraph(stripLegacyRichTextClasses(editor.getHTML()))
);
},
editorProps,
immediatelyRender: false,
@@ -480,7 +486,9 @@ const RichTextEditor = ({
});
useEffect(() => {
const normalizedContent = stripLegacyRichTextClasses(content);
const normalizedContent = stripTrailingListParagraph(
stripLegacyRichTextClasses(content)
);
if (editor && normalizedContent !== editor.getHTML()) {
editor.commands.setContent(normalizedContent, { emitUpdate: false });
+18 -2
View File
@@ -3,6 +3,8 @@ const EMPTY_PARAGRAPH_REGEX = /<p>(?:\s|&nbsp;|<br\s*\/?>)*<\/p>/gi;
const HTML_BREAK_REGEX = /<br\s*\/?>/gi;
const HTML_ANY_TAG_REGEX = /<\/?[^>]+>/g;
const INVISIBLE_WHITESPACE_REGEX = /[\s\u200B-\u200D\uFEFF]/g;
const TRAILING_LIST_PARAGRAPH_REGEX =
/(<\/(?:ul|ol)>)\s*<p>(?:\s|&nbsp;|<br\s*\/?>)*<\/p>\s*$/i;
const RICH_TEXT_ANCHOR_REGEX = /<a\b([^>]*)>/gi;
const CLASS_ATTRIBUTE_REGEX = /\bclass\s*=\s*("([^"]*)"|'([^']*)')/i;
const CLASS_ATTRIBUTE_GLOBAL_REGEX = /\bclass\s*=\s*("([^"]*)"|'([^']*)')/gi;
@@ -47,13 +49,25 @@ export const stripLegacyRichTextClasses = (content?: string) => {
const classes = currentValue
.split(/\s+/)
.filter(Boolean)
.filter((className) => !LEGACY_RICH_TEXT_CLASSES.has(className));
.filter((className: string) => !LEGACY_RICH_TEXT_CLASSES.has(className));
return classes.length ? `class="${classes.join(" ")}"` : "";
}
);
};
export const stripTrailingListParagraph = (content?: string) => {
if (!content) return "";
let normalized = content;
while (TRAILING_LIST_PARAGRAPH_REGEX.test(normalized)) {
normalized = normalized.replace(TRAILING_LIST_PARAGRAPH_REGEX, "$1");
}
return normalized;
};
export const normalizeLinkHref = (href?: string) => {
if (!href) return null;
@@ -97,7 +111,9 @@ export const normalizeRichTextContent = (content?: string) => {
normalized = escapeHtml(content).replace(/\r\n|\r|\n/g, "<br />");
}
return decorateRichTextAnchors(stripLegacyRichTextClasses(normalized)).replace(
return decorateRichTextAnchors(
stripTrailingListParagraph(stripLegacyRichTextClasses(normalized))
).replace(
EMPTY_PARAGRAPH_REGEX,
"<p><br /></p>"
);