/** * 在浏览器 DOM 中重新构造允许的富文本,不直接信任后端商品/内容 HTML。 * 白名单外的普通容器展开子内容;script、表单和非 HTML 命名空间整段丢弃。 */ var e = new Set( `a.p.br.hr.div.span.h1.h2.h3.h4.h5.h6.b.strong.i.em.u.s.del.ins.sub.sup.small.mark.blockquote.pre.code.kbd.ul.ol.li.dl.dt.dd.table.thead.tbody.tfoot.tr.th.td.caption.colgroup.col.img.figure.figcaption`.split( `.`, ), ), t = new Set([ `script`, `style`, `iframe`, `object`, `embed`, `link`, `meta`, `base`, `form`, `input`, `button`, `textarea`, `select`, `option`, `template`, `noscript`, `noembed`, `noframes`, `xmp`, `plaintext`, `svg`, `math`, `audio`, `video`, `source`, ]); // 图片额外允许有限位图类型的 base64 data URL;普通链接可用 mailto/tel,不允许脚本协议。 function n(e, t = !1) { if (!e || /[\u0000-\u001f\u007f]/.test(e)) return !1; if ( t && /^data:image\/(?:png|jpeg|gif|webp|avif);base64,[a-z\d+/]+={0,2}$/i.test(e) ) return !0; try { let n = new URL(e, `https://sanitizer.invalid/`).protocol; return ( n === `https:` || n === `http:` || (!t && (n === `mailto:` || n === `tel:`)) ); } catch { return !1; } } /** 清洗标签和属性,只保留有限排版;新窗口链接强制添加 noopener noreferrer。 */ function r(r) { if (!r) return ``; let i = new DOMParser().parseFromString(r, `text/html`), a = i.createElement(`div`); function o(r, a) { if (r.nodeType === 3) { a.appendChild(i.createTextNode(r.textContent ?? ``)); return; } if (r.nodeType !== 1) return; let s = r, c = s.localName; if (s.namespaceURI !== `http://www.w3.org/1999/xhtml` || t.has(c)) return; let l = a; if (e.has(c)) { let e = i.createElement(c); for (let t of Array.from(s.attributes)) { let r = t.name.toLowerCase(), i = t.value.trim(); t.namespaceURI === null && (r === `title` || (r === `alt` && c === `img`) ? e.setAttribute(r, t.value) : r === `style` && /^(p|div|h[1-6])$/.test(c) && /^text-align:\s*(left|center|right|justify);?$/i.test(i) ? e.setAttribute(`style`, i.toLowerCase()) : ((r === `dir` && /^(ltr|rtl|auto)$/.test(i)) || (r === `href` && c === `a` && n(i)) || (r === `src` && c === `img` && n(i, !0)) || (r === `target` && c === `a` && /^(?:_blank|_self)$/.test(i)) || (r === `scope` && c === `th` && /^(?:row|col|rowgroup|colgroup)$/.test(i)) || ((((r === `colspan` || r === `rowspan`) && (c === `td` || c === `th`)) || ((r === `width` || r === `height`) && c === `img`) || (r === `span` && (c === `col` || c === `colgroup`)) || (r === `start` && c === `ol`)) && /^\d{1,4}$/.test(i) && Number(i) > 0)) && e.setAttribute(r, i)); } (c === `a` && e.getAttribute(`target`) === `_blank` && e.setAttribute(`rel`, `noopener noreferrer`), a.appendChild(e), (l = e)); } for (let e of Array.from(r.childNodes)) o(e, l); } for (let e of Array.from(i.body.childNodes)) o(e, a); return a.innerHTML; } // 对历史双重实体编码最多解码两次;返回值只是解码文本,不能代替上面的 HTML 清洗。 function i(e) { if (!e) return ``; let t = e; for ( let e = 0; e < 2 && /&(?:amp|lt|gt|quot|#\d+|#x[0-9a-f]+);/i.test(t); e++ ) { let e = new DOMParser().parseFromString(t, `text/html`).documentElement .textContent ?? t; if (e === t) break; t = e; } return t; } export { r as n, i as t };