// 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(); }