CI / test (push) Successful in 50s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
// Product descriptions: HTML, as on IMVU, made safe by an allowlist (docs/design.md, "Product info"). Cleaned when
|
|
// saved; whatever shows them cleans again.
|
|
import sanitize from "sanitize-html";
|
|
|
|
export const maxDescription = 20_000;
|
|
|
|
const color = [/^#[0-9a-f]{3,8}$/i, /^[a-z]{3,20}$/i, /^rgba?\(\s*[\d.\s,%]+\)$/i];
|
|
const size = [/^\d{1,3}(\.\d+)?(px|pt|em|rem|%)$/, /^(xx-small|x-small|small|medium|large|x-large|xx-large)$/];
|
|
|
|
const options: sanitize.IOptions = {
|
|
allowedTags: [
|
|
"b", "i", "u", "s", "em", "strong", "small", "big", "sub", "sup", "font", "span", "div", "p", "br", "hr",
|
|
"center", "ul", "ol", "li", "table", "thead", "tbody", "tr", "td", "th", "h1", "h2", "h3", "h4", "h5", "h6",
|
|
"a", "img",
|
|
],
|
|
allowedAttributes: {
|
|
a: ["href", "rel", "target"], // rel and target are always set by the transform below
|
|
img: ["src", "alt", "width", "height"],
|
|
font: ["color", "size"],
|
|
td: ["colspan", "rowspan"],
|
|
th: ["colspan", "rowspan"],
|
|
"*": ["style"],
|
|
},
|
|
allowedSchemes: ["http", "https"],
|
|
allowedSchemesByTag: { img: ["https"] },
|
|
allowProtocolRelative: false,
|
|
allowedStyles: {
|
|
"*": {
|
|
color,
|
|
"background-color": color,
|
|
"font-size": size,
|
|
"font-weight": [/^(normal|bold|bolder|lighter|[1-9]00)$/],
|
|
"font-style": [/^(normal|italic|oblique)$/],
|
|
"text-align": [/^(left|right|center|justify)$/],
|
|
"text-decoration": [/^(none|underline|line-through|overline)$/],
|
|
},
|
|
},
|
|
transformTags: {
|
|
a: (tagName, attribs) => ({
|
|
tagName,
|
|
attribs: { ...attribs, rel: "nofollow ugc noopener noreferrer", target: "_blank" },
|
|
}),
|
|
},
|
|
// Images need an absolute https address (scheme rules don't apply to relative ones); others are dropped.
|
|
exclusiveFilter: (frame) => frame.tag === "img" && !/^https:\/\//i.test(frame.attribs.src ?? ""),
|
|
disallowedTagsMode: "discard",
|
|
};
|
|
|
|
export function cleanHTML(html: string): string {
|
|
return sanitize(html, options).trim();
|
|
}
|