File size: 4,073 Bytes
aa1ee73 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | const HTML_TAG_REGEX = /<\/?[a-z][\s\S]*>/i;
const EMPTY_PARAGRAPH_REGEX = /<p>(?:\s| |<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| |<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;
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const DOMAIN_REGEX =
/^(?:www\.)?[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+(?:[/?#][^\s]*)?$/i;
const SAFE_LINK_PROTOCOL_REGEX = /^(https?:|mailto:|tel:)/i;
const ANY_PROTOCOL_REGEX = /^[a-z][a-z\d+\-.]*:/i;
const LEGACY_RICH_TEXT_CLASSES = new Set(["custom-list", "custom-list-ordered"]);
const escapeHtml = (text: string) =>
text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
const decorateRichTextAnchors = (content: string) =>
content.replace(RICH_TEXT_ANCHOR_REGEX, (match, attrs: string) => {
if (CLASS_ATTRIBUTE_REGEX.test(attrs)) {
return match.replace(CLASS_ATTRIBUTE_REGEX, (_classMatch, quotedValue, doubleQuoted, singleQuoted) => {
const currentValue = doubleQuoted ?? singleQuoted ?? quotedValue ?? "";
const classes = currentValue.split(/\s+/).filter(Boolean);
if (!classes.includes("rich-text-link")) {
classes.push("rich-text-link");
}
return `class="${classes.join(" ")}"`;
});
}
return `<a class="rich-text-link"${attrs}>`;
});
export const stripLegacyRichTextClasses = (content?: string) => {
if (!content) return "";
return content.replace(
CLASS_ATTRIBUTE_GLOBAL_REGEX,
(_match, quotedValue, doubleQuoted, singleQuoted) => {
const currentValue = doubleQuoted ?? singleQuoted ?? quotedValue ?? "";
const classes = currentValue
.split(/\s+/)
.filter(Boolean)
.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;
const value = href.trim();
if (!value) return null;
if (SAFE_LINK_PROTOCOL_REGEX.test(value)) {
return value;
}
if (value.startsWith("//")) {
return `https:${value}`;
}
if (EMAIL_REGEX.test(value)) {
return `mailto:${value}`;
}
if (DOMAIN_REGEX.test(value)) {
return `https://${value}`;
}
if (ANY_PROTOCOL_REGEX.test(value)) {
return null;
}
return null;
};
/**
* 规范化富文本内容,解决以下问题:
* 1. 纯文本中的换行无法在 HTML 中展示;
* 2. TipTap 产生的空 <p> 标签没有高度。
*/
export const normalizeRichTextContent = (content?: string) => {
if (!content) return "";
let normalized = content;
if (!HTML_TAG_REGEX.test(content)) {
normalized = escapeHtml(content).replace(/\r\n|\r|\n/g, "<br />");
}
return decorateRichTextAnchors(
stripTrailingListParagraph(stripLegacyRichTextClasses(normalized))
).replace(
EMPTY_PARAGRAPH_REGEX,
"<p><br /></p>"
);
};
export const hasMeaningfulRichTextContent = (content?: string) => {
if (!content) return false;
if (!HTML_TAG_REGEX.test(content)) {
return content.replace(INVISIBLE_WHITESPACE_REGEX, "").length > 0;
}
const plainText = content
.replace(EMPTY_PARAGRAPH_REGEX, "")
.replace(HTML_BREAK_REGEX, "")
.replace(/ /gi, " ")
.replace(HTML_ANY_TAG_REGEX, "")
.replace(INVISIBLE_WHITESPACE_REGEX, "");
return plainText.length > 0;
};
|