feat : 6 astra improve it
CI / Verify and audit (push) Successful in 2m33s
CI / Build, scan and deploy immutable images (push) Failing after 1m29s

This commit is contained in:
2026-09-22 18:28:18 +07:00 Unverified
parent 88afa8e947
commit 87e6bcd96f
56 changed files with 1351 additions and 511 deletions
+22 -39
View File
@@ -1,3 +1,5 @@
import sanitizeHtml from "sanitize-html";
import { highlightTextSegments, type TextHighlight } from "@/lib/guides/highlights";
const ALLOWED_TAGS = new Set([
@@ -17,10 +19,6 @@ const ALLOWED_TAGS = new Set([
"ul",
]);
const BLOCKED_CONTENT = /<(script|style|iframe|object|embed|svg|math|template)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
const HTML_COMMENT = /<!--[\s\S]*?-->/g;
const TAG = /<\/?([a-zA-Z][a-zA-Z0-9-]*)([^>]*)>/g;
const ATTRIBUTE = /([a-zA-Z_:][a-zA-Z0-9_:.-]*)\s*=\s*("[^"]*"|'[^']*'|[^\s"'=<>`]+)/g;
const HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
const RGB_COLOR = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/i;
@@ -67,44 +65,29 @@ function escapeHtml(value: string): string {
return escapeAttribute(value).replaceAll("'", "&#39;");
}
function attributeValue(rawAttributes: string, name: string): string {
for (const match of rawAttributes.matchAll(ATTRIBUTE)) {
if (match[1].toLowerCase() === name) {
return match[2].replace(/^['"]|['"]$/g, "");
}
}
return "";
}
export function sanitizeRichTextHtml(value: string): string {
return value
.replace(BLOCKED_CONTENT, "")
.replace(HTML_COMMENT, "")
.replace(TAG, (tag, rawName: string, rawAttributes: string) => {
const name = rawName.toLowerCase();
if (!ALLOWED_TAGS.has(name)) return "";
if (tag.startsWith("</")) return name === "br" ? "" : `</${name}>`;
if (name === "br") return "<br>";
if (name === "a") {
const href = sanitizeRichTextUrl(attributeValue(rawAttributes, "href"));
return href
? `<a href="${escapeAttribute(href)}" target="_blank" rel="noopener noreferrer">`
: "<a>";
}
if (name === "span") {
const style = attributeValue(rawAttributes, "style");
return sanitizeHtml(value, {
allowedTags: [...ALLOWED_TAGS],
allowedAttributes: { a: ["href", "target", "rel"], span: ["style"] },
allowedSchemes: ["http", "https", "mailto"],
allowProtocolRelative: false,
nonTextTags: ["script", "style", "textarea", "option", "iframe", "object", "embed", "svg", "math", "template"],
transformTags: {
a: (_name, attributes): sanitizeHtml.Tag => {
const href = sanitizeRichTextUrl(attributes.href ?? "");
return { tagName: "a", attribs: href
? { href, target: "_blank", rel: "noopener noreferrer" } : {} };
},
span: (_name, attributes): sanitizeHtml.Tag => {
const color = normalizeRichTextColor(
style.match(/(?:^|;)\s*color\s*:\s*([^;]+?)\s*(?:;|$)/i)?.[1] ?? "",
(attributes.style ?? "").match(/(?:^|;)\s*color\s*:\s*([^;]+?)\s*(?:;|$)/i)?.[1] ?? "",
);
return color
? `<span style="color: ${color}">`
: "<span>";
}
return `<${name}>`;
});
return { tagName: "span", attribs: color ? { style: `color: ${color}` } : {} };
},
},
// Colors have already been normalized against our strict hex/RGB parser.
parseStyleAttributes: false,
}).replace(/<br\s*\/>/g, "<br>");
}
function isRichTextHtml(value: string): boolean {