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