@favish/staffbase-utils 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html.cjs.js +1 -1
- package/dist/html.cjs.js.map +1 -1
- package/dist/html.es.mjs +30 -10
- package/dist/html.es.mjs.map +1 -1
- package/dist/src/html/customElementHandling.d.ts +12 -0
- package/dist/src/html/customElementHandling.d.ts.map +1 -0
- package/dist/src/html/logRemovedEmbeds.d.ts +11 -0
- package/dist/src/html/logRemovedEmbeds.d.ts.map +1 -0
- package/dist/src/html/sanitizeArticleHtml.d.ts.map +1 -1
- package/dist/src/html/sanitizeHtml.d.ts +3 -1
- package/dist/src/html/sanitizeHtml.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/html.cjs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require("dompurify");c=s(c);var l=e=>{if(!e)return``;try{return c.default.sanitize(e,{ALLOWED_TAGS:[],KEEP_CONTENT:!0})}catch{return e.replace(/<\/?[^>]+(>|$)/g,``)}},u=e=>l(e).toLowerCase().replace(/[.,!?;:"'()[\]\-_/\\]/g,` `).replace(/\s+/g,` `).trim(),d=(0,c.default)(window),
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require("dompurify");c=s(c);var l=e=>{if(!e)return``;try{return c.default.sanitize(e,{ALLOWED_TAGS:[],KEEP_CONTENT:!0})}catch{return e.replace(/<\/?[^>]+(>|$)/g,``)}},u=e=>l(e).toLowerCase().replace(/[.,!?;:"'()[\]\-_/\\]/g,` `).replace(/\s+/g,` `).trim(),d={tagNameCheck:/^[a-z][a-z0-9]*-[a-z0-9-]*$/,attributeNameCheck:/^(?!on)[a-z][a-z0-9]*([-:][a-z0-9]+)*$/,allowCustomizedBuiltInElements:!0},f=e=>{let t=[];for(let n of e){if(!n||typeof n!=`object`||!(`element`in n))continue;let e=n.element;if(e instanceof Element){let n=e.tagName.toLowerCase();n.includes(`-`)&&t.push(n)}}t.length>0&&console.warn(`[staffbase-utils] sanitizer removed ${t.length} possible widget embed(s): ${t.join(`, `)} — they will not render. If these are valid embeds, the sanitizer config may need updating.`)},p=(0,c.default)(window),m=null;p.addHook(`afterSanitizeAttributes`,e=>{if(e instanceof Element&&(e.tagName===`A`&&e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`),e.tagName===`IFRAME`&&m)){let t=e.getAttribute(`src`)??``;m(t)||e.parentNode?.removeChild(e)}});var h=(e,t={})=>{m=t.isAllowedIframeSrc??null;try{let t=p.sanitize(e,{USE_PROFILES:{html:!0},CUSTOM_ELEMENT_HANDLING:d,ADD_TAGS:[`iframe`],ADD_ATTR:[`target`,`allow`,`allowfullscreen`,`frameborder`,`scrolling`,`loading`,`referrerpolicy`],FORBID_TAGS:[`script`,`style`],FORBID_ATTR:[`onerror`,`onload`,`onclick`]});return f(p.removed),t}finally{m=null}},g=e=>{let t=c.default.sanitize(e,{CUSTOM_ELEMENT_HANDLING:d});return f(c.default.removed),t};exports.cleanHTML=u,exports.sanitizeArticleHtml=h,exports.sanitizeHtml=g,exports.stripHtmlTags=l;
|
|
2
2
|
//# sourceMappingURL=html.cjs.js.map
|
package/dist/html.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.cjs.js","names":[],"sources":["../src/html/stripHtmlTags.ts","../src/html/cleanHTML.ts","../src/html/sanitizeArticleHtml.ts","../src/html/sanitizeHtml.ts"],"sourcesContent":["import DOMPurify from 'dompurify'\n\n/**\n * Strips all HTML tags, keeping the text content. From smart-search's\n * stripHtmlTags; its html-react-parser fallback is intentionally dropped to\n * avoid a heavy runtime dependency — the DOMPurify path plus a regex fallback is\n * sufficient and never throws in practice.\n * @param {string | undefined} html - String that may contain HTML tags.\n * @returns {string} The text content with tags removed.\n */\nexport const stripHtmlTags = (html?: string): string => {\n if (!html) return ''\n\n try {\n return DOMPurify.sanitize(html, { ALLOWED_TAGS: [], KEEP_CONTENT: true })\n } catch {\n return html.replace(/<\\/?[^>]+(>|$)/g, '')\n }\n}\n","import { stripHtmlTags } from './stripHtmlTags'\n\n/**\n * Normalizes HTML into a lowercase, punctuation-free, single-spaced token string\n * for search indexing/matching. From alerts' cleanHTML; builds on stripHtmlTags\n * so tag removal stays single-sourced.\n * @param {string} html - The HTML string to clean.\n * @returns {string} The cleaned, normalized text.\n */\nexport const cleanHTML = (html: string): string =>\n stripHtmlTags(html)\n .toLowerCase()\n .replace(/[.,!?;:\"'()[\\]\\-_/\\\\]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n","import createDOMPurify from 'dompurify'\n\nimport type { SanitizeArticleHtmlOptions } from '../types/html/SanitizeArticleHtmlOptions'\n\n// Dedicated DOMPurify instance so the hardening hook below is scoped to this\n// sanitizer and never pollutes the consumer's shared default DOMPurify instance\n// (other code may sanitize through the default instance directly).\nconst purifier = createDOMPurify(window)\n\n// Set synchronously around each (synchronous) sanitize call. JS is\n// single-threaded, so the guard cannot leak across calls.\nlet activeIframeGuard: ((src: string) => boolean) | null = null\n\n// afterSanitizeAttributes hook: force rel=\"noopener noreferrer\" on\n// target=\"_blank\" anchors (reverse-tabnabbing), and drop iframes whose src fails\n// the injected allowlist predicate when one is active.\npurifier.addHook('afterSanitizeAttributes', (node) => {\n if (!(node instanceof Element)) return\n\n if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n node.setAttribute('rel', 'noopener noreferrer')\n }\n\n if (node.tagName === 'IFRAME' && activeIframeGuard) {\n const src = node.getAttribute('src') ?? ''\n if (!activeIframeGuard(src)) node.parentNode?.removeChild(node)\n }\n})\n\n/**\n * Canonical sanitizer for rich article HTML rendered into a session-bearing\n * webview. Superset of the alerts and unacknowledged-bulletins variants: keeps\n * iframes and data-* attributes (renderWidgets discovers embedded sub-widgets via\n * data-*), strips scripts/inline handlers/javascript:, and forces\n * rel=\"noopener noreferrer\" on target=\"_blank\" anchors. When options.isAllowedIframeSrc\n * is provided, iframes whose src fails it are dropped (injected so this module\n * does not depend on /links, which owns isAllowedIframeSrc).\n * @param {string} html - Raw article HTML from the Staffbase API.\n * @param {SanitizeArticleHtmlOptions} options - Optional iframe-src allowlist.\n * @returns {string} Sanitized HTML safe to inject/parse.\n */\nexport const sanitizeArticleHtml = (\n html: string,\n options: SanitizeArticleHtmlOptions = {},\n): string => {\n activeIframeGuard = options.isAllowedIframeSrc ?? null\n try {\n
|
|
1
|
+
{"version":3,"file":"html.cjs.js","names":[],"sources":["../src/html/stripHtmlTags.ts","../src/html/cleanHTML.ts","../src/html/customElementHandling.ts","../src/html/logRemovedEmbeds.ts","../src/html/sanitizeArticleHtml.ts","../src/html/sanitizeHtml.ts"],"sourcesContent":["import DOMPurify from 'dompurify'\n\n/**\n * Strips all HTML tags, keeping the text content. From smart-search's\n * stripHtmlTags; its html-react-parser fallback is intentionally dropped to\n * avoid a heavy runtime dependency — the DOMPurify path plus a regex fallback is\n * sufficient and never throws in practice.\n * @param {string | undefined} html - String that may contain HTML tags.\n * @returns {string} The text content with tags removed.\n */\nexport const stripHtmlTags = (html?: string): string => {\n if (!html) return ''\n\n try {\n return DOMPurify.sanitize(html, { ALLOWED_TAGS: [], KEEP_CONTENT: true })\n } catch {\n return html.replace(/<\\/?[^>]+(>|$)/g, '')\n }\n}\n","import { stripHtmlTags } from './stripHtmlTags'\n\n/**\n * Normalizes HTML into a lowercase, punctuation-free, single-spaced token string\n * for search indexing/matching. From alerts' cleanHTML; builds on stripHtmlTags\n * so tag removal stays single-sourced.\n * @param {string} html - The HTML string to clean.\n * @returns {string} The cleaned, normalized text.\n */\nexport const cleanHTML = (html: string): string =>\n stripHtmlTags(html)\n .toLowerCase()\n .replace(/[.,!?;:\"'()[\\]\\-_/\\\\]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n","import type { Config } from 'dompurify'\n\n/**\n * DOMPurify custom-element handling that preserves embedded-widget custom\n * elements (any hyphenated tag, e.g. `<news-teaser>`) and their kebab-case /\n * `data-` attributes so the host widget manager (renderWidgets) can hydrate\n * them. Without this, DOMPurify's default profile silently strips unknown custom\n * elements AND their attributes, wiping embeds out of the content before they can\n * render. `on*` event-handler attributes are still rejected (defense in depth\n * alongside DOMPurify's own XSS stripping). Promoted from staffbase-global-content.\n */\nexport const customElementHandling: NonNullable<\n Config['CUSTOM_ELEMENT_HANDLING']\n> = {\n // Any valid custom-element tag name (must contain a hyphen per the spec).\n tagNameCheck: /^[a-z][a-z0-9]*-[a-z0-9-]*$/,\n // Allow kebab-case / data- attributes on custom elements, but never `on*`.\n attributeNameCheck: /^(?!on)[a-z][a-z0-9]*([-:][a-z0-9]+)*$/,\n // Permit `<div is=\"some-widget\">`-style customized built-in elements.\n allowCustomizedBuiltInElements: true,\n}\n","/**\n * Warns (once per call) when the sanitizer dropped what looks like an embedded\n * widget — a custom-element tag (any hyphenated tag name). These removals are the\n * silent failure mode that makes embedded widgets \"disappear\" before the host can\n * render them, so surfacing them aids diagnosis. Scripts, styles and on*\n * handlers are intentionally removed and are NOT reported (they would be noise).\n * @param {readonly unknown[]} removed - DOMPurify's `removed` array after sanitize.\n * @returns {void}\n */\nexport const logRemovedEmbeds = (removed: readonly unknown[]): void => {\n const tags: string[] = []\n\n for (const entry of removed) {\n if (!entry || typeof entry !== 'object' || !('element' in entry)) continue\n const element = (entry as { element: unknown }).element\n if (element instanceof Element) {\n const tag = element.tagName.toLowerCase()\n if (tag.includes('-')) tags.push(tag)\n }\n }\n\n if (tags.length > 0) {\n console.warn(\n `[staffbase-utils] sanitizer removed ${tags.length} possible widget embed(s): ${tags.join(', ')} — they will not render. If these are valid embeds, the sanitizer config may need updating.`,\n )\n }\n}\n","import createDOMPurify from 'dompurify'\n\nimport type { SanitizeArticleHtmlOptions } from '../types/html/SanitizeArticleHtmlOptions'\nimport { customElementHandling } from './customElementHandling'\nimport { logRemovedEmbeds } from './logRemovedEmbeds'\n\n// Dedicated DOMPurify instance so the hardening hook below is scoped to this\n// sanitizer and never pollutes the consumer's shared default DOMPurify instance\n// (other code may sanitize through the default instance directly).\nconst purifier = createDOMPurify(window)\n\n// Set synchronously around each (synchronous) sanitize call. JS is\n// single-threaded, so the guard cannot leak across calls.\nlet activeIframeGuard: ((src: string) => boolean) | null = null\n\n// afterSanitizeAttributes hook: force rel=\"noopener noreferrer\" on\n// target=\"_blank\" anchors (reverse-tabnabbing), and drop iframes whose src fails\n// the injected allowlist predicate when one is active.\npurifier.addHook('afterSanitizeAttributes', (node) => {\n if (!(node instanceof Element)) return\n\n if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n node.setAttribute('rel', 'noopener noreferrer')\n }\n\n if (node.tagName === 'IFRAME' && activeIframeGuard) {\n const src = node.getAttribute('src') ?? ''\n if (!activeIframeGuard(src)) node.parentNode?.removeChild(node)\n }\n})\n\n/**\n * Canonical sanitizer for rich article HTML rendered into a session-bearing\n * webview. Superset of the alerts and unacknowledged-bulletins variants: keeps\n * iframes and data-* attributes (renderWidgets discovers embedded sub-widgets via\n * data-*), strips scripts/inline handlers/javascript:, and forces\n * rel=\"noopener noreferrer\" on target=\"_blank\" anchors. When options.isAllowedIframeSrc\n * is provided, iframes whose src fails it are dropped (injected so this module\n * does not depend on /links, which owns isAllowedIframeSrc).\n * @param {string} html - Raw article HTML from the Staffbase API.\n * @param {SanitizeArticleHtmlOptions} options - Optional iframe-src allowlist.\n * @returns {string} Sanitized HTML safe to inject/parse.\n */\nexport const sanitizeArticleHtml = (\n html: string,\n options: SanitizeArticleHtmlOptions = {},\n): string => {\n activeIframeGuard = options.isAllowedIframeSrc ?? null\n try {\n const clean = purifier.sanitize(html, {\n USE_PROFILES: { html: true },\n CUSTOM_ELEMENT_HANDLING: customElementHandling,\n ADD_TAGS: ['iframe'],\n ADD_ATTR: [\n 'target',\n 'allow',\n 'allowfullscreen',\n 'frameborder',\n 'scrolling',\n 'loading',\n 'referrerpolicy',\n ],\n FORBID_TAGS: ['script', 'style'],\n FORBID_ATTR: ['onerror', 'onload', 'onclick'],\n })\n logRemovedEmbeds(purifier.removed)\n return clean\n } finally {\n activeIframeGuard = null\n }\n}\n","import DOMPurify from 'dompurify'\n\nimport { customElementHandling } from './customElementHandling'\nimport { logRemovedEmbeds } from './logRemovedEmbeds'\n\n/**\n * Strict sanitizer for untrusted snippet/teaser HTML rendered through a\n * non-sanitizing parser (e.g. html-react-parser). The default DOMPurify profile\n * strips scripts, inline handlers and dangerous URLs AND drops iframes, keeping\n * only basic rich-text markup. Embedded-widget custom elements (and their\n * kebab/data attributes) are preserved so the host can hydrate them. From\n * global-content's sanitizeHtml.\n *\n * Use sanitizeArticleHtml instead when rendering full article bodies that must\n * keep iframes / data-* embeds.\n * @param {string} html - Raw HTML string from the API.\n * @returns {string} Sanitized HTML.\n */\nexport const sanitizeHtml = (html: string): string => {\n const clean = DOMPurify.sanitize(html, {\n CUSTOM_ELEMENT_HANDLING: customElementHandling,\n })\n logRemovedEmbeds(DOMPurify.removed)\n return clean\n}\n"],"mappings":"mkBAUA,IAAa,EAAiB,GAA0B,CACtD,GAAI,CAAC,EAAM,MAAO,GAElB,GAAI,CACF,OAAO,EAAA,QAAU,SAAS,EAAM,CAAE,aAAc,CAAC,EAAG,aAAc,EAAK,CAAC,CAC1E,MAAQ,CACN,OAAO,EAAK,QAAQ,kBAAmB,EAAE,CAC3C,CACF,ECTa,EAAa,GACxB,EAAc,CAAI,EACf,YAAY,EACZ,QAAQ,yBAA0B,GAAG,EACrC,QAAQ,OAAQ,GAAG,EACnB,KAAK,ECHG,EAET,CAEF,aAAc,8BAEd,mBAAoB,yCAEpB,+BAAgC,EAClC,ECXa,EAAoB,GAAsC,CACrE,IAAM,EAAiB,CAAC,EAExB,IAAK,IAAM,KAAS,EAAS,CAC3B,GAAI,CAAC,GAAS,OAAO,GAAU,UAAY,EAAE,YAAa,GAAQ,SAClE,IAAM,EAAW,EAA+B,QAChD,GAAI,aAAmB,QAAS,CAC9B,IAAM,EAAM,EAAQ,QAAQ,YAAY,EACpC,EAAI,SAAS,GAAG,GAAG,EAAK,KAAK,CAAG,CACtC,CACF,CAEI,EAAK,OAAS,GAChB,QAAQ,KACN,uCAAuC,EAAK,OAAO,6BAA6B,EAAK,KAAK,IAAI,EAAE,4FAClG,CAEJ,ECjBM,GAAA,EAAA,EAAA,SAA2B,MAAM,EAInC,EAAuD,KAK3D,EAAS,QAAQ,0BAA4B,GAAS,CAC9C,gBAAgB,UAElB,EAAK,UAAY,KAAO,EAAK,aAAa,QAAQ,IAAM,UAC1D,EAAK,aAAa,MAAO,qBAAqB,EAG5C,EAAK,UAAY,UAAY,GAAmB,CAClD,IAAM,EAAM,EAAK,aAAa,KAAK,GAAK,GACnC,EAAkB,CAAG,GAAG,EAAK,YAAY,YAAY,CAAI,CAChE,CACF,CAAC,EAcD,IAAa,GACX,EACA,EAAsC,CAAC,IAC5B,CACX,EAAoB,EAAQ,oBAAsB,KAClD,GAAI,CACF,IAAM,EAAQ,EAAS,SAAS,EAAM,CACpC,aAAc,CAAE,KAAM,EAAK,EAC3B,wBAAyB,EACzB,SAAU,CAAC,QAAQ,EACnB,SAAU,CACR,SACA,QACA,kBACA,cACA,YACA,UACA,gBACF,EACA,YAAa,CAAC,SAAU,OAAO,EAC/B,YAAa,CAAC,UAAW,SAAU,SAAS,CAC9C,CAAC,EAED,OADA,EAAiB,EAAS,OAAO,EAC1B,CACT,QAAU,CACR,EAAoB,IACtB,CACF,ECpDa,EAAgB,GAAyB,CACpD,IAAM,EAAQ,EAAA,QAAU,SAAS,EAAM,CACrC,wBAAyB,CAC3B,CAAC,EAED,OADA,EAAiB,EAAA,QAAU,OAAO,EAC3B,CACT"}
|
package/dist/html.es.mjs
CHANGED
|
@@ -10,18 +10,34 @@ var t = (t) => {
|
|
|
10
10
|
} catch {
|
|
11
11
|
return t.replace(/<\/?[^>]+(>|$)/g, "");
|
|
12
12
|
}
|
|
13
|
-
}, n = (e) => t(e).toLowerCase().replace(/[.,!?;:"'()[\]\-_/\\]/g, " ").replace(/\s+/g, " ").trim(), r =
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
}, n = (e) => t(e).toLowerCase().replace(/[.,!?;:"'()[\]\-_/\\]/g, " ").replace(/\s+/g, " ").trim(), r = {
|
|
14
|
+
tagNameCheck: /^[a-z][a-z0-9]*-[a-z0-9-]*$/,
|
|
15
|
+
attributeNameCheck: /^(?!on)[a-z][a-z0-9]*([-:][a-z0-9]+)*$/,
|
|
16
|
+
allowCustomizedBuiltInElements: !0
|
|
17
|
+
}, i = (e) => {
|
|
18
|
+
let t = [];
|
|
19
|
+
for (let n of e) {
|
|
20
|
+
if (!n || typeof n != "object" || !("element" in n)) continue;
|
|
21
|
+
let e = n.element;
|
|
22
|
+
if (e instanceof Element) {
|
|
23
|
+
let n = e.tagName.toLowerCase();
|
|
24
|
+
n.includes("-") && t.push(n);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
t.length > 0 && console.warn(`[staffbase-utils] sanitizer removed ${t.length} possible widget embed(s): ${t.join(", ")} — they will not render. If these are valid embeds, the sanitizer config may need updating.`);
|
|
28
|
+
}, a = e(window), o = null;
|
|
29
|
+
a.addHook("afterSanitizeAttributes", (e) => {
|
|
30
|
+
if (e instanceof Element && (e.tagName === "A" && e.getAttribute("target") === "_blank" && e.setAttribute("rel", "noopener noreferrer"), e.tagName === "IFRAME" && o)) {
|
|
16
31
|
let t = e.getAttribute("src") ?? "";
|
|
17
|
-
|
|
32
|
+
o(t) || e.parentNode?.removeChild(e);
|
|
18
33
|
}
|
|
19
34
|
});
|
|
20
|
-
var
|
|
21
|
-
|
|
35
|
+
var s = (e, t = {}) => {
|
|
36
|
+
o = t.isAllowedIframeSrc ?? null;
|
|
22
37
|
try {
|
|
23
|
-
|
|
38
|
+
let t = a.sanitize(e, {
|
|
24
39
|
USE_PROFILES: { html: !0 },
|
|
40
|
+
CUSTOM_ELEMENT_HANDLING: r,
|
|
25
41
|
ADD_TAGS: ["iframe"],
|
|
26
42
|
ADD_ATTR: [
|
|
27
43
|
"target",
|
|
@@ -39,11 +55,15 @@ var a = (e, t = {}) => {
|
|
|
39
55
|
"onclick"
|
|
40
56
|
]
|
|
41
57
|
});
|
|
58
|
+
return i(a.removed), t;
|
|
42
59
|
} finally {
|
|
43
|
-
|
|
60
|
+
o = null;
|
|
44
61
|
}
|
|
45
|
-
},
|
|
62
|
+
}, c = (t) => {
|
|
63
|
+
let n = e.sanitize(t, { CUSTOM_ELEMENT_HANDLING: r });
|
|
64
|
+
return i(e.removed), n;
|
|
65
|
+
};
|
|
46
66
|
//#endregion
|
|
47
|
-
export { n as cleanHTML,
|
|
67
|
+
export { n as cleanHTML, s as sanitizeArticleHtml, c as sanitizeHtml, t as stripHtmlTags };
|
|
48
68
|
|
|
49
69
|
//# sourceMappingURL=html.es.mjs.map
|
package/dist/html.es.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.es.mjs","names":[],"sources":["../src/html/stripHtmlTags.ts","../src/html/cleanHTML.ts","../src/html/sanitizeArticleHtml.ts","../src/html/sanitizeHtml.ts"],"sourcesContent":["import DOMPurify from 'dompurify'\n\n/**\n * Strips all HTML tags, keeping the text content. From smart-search's\n * stripHtmlTags; its html-react-parser fallback is intentionally dropped to\n * avoid a heavy runtime dependency — the DOMPurify path plus a regex fallback is\n * sufficient and never throws in practice.\n * @param {string | undefined} html - String that may contain HTML tags.\n * @returns {string} The text content with tags removed.\n */\nexport const stripHtmlTags = (html?: string): string => {\n if (!html) return ''\n\n try {\n return DOMPurify.sanitize(html, { ALLOWED_TAGS: [], KEEP_CONTENT: true })\n } catch {\n return html.replace(/<\\/?[^>]+(>|$)/g, '')\n }\n}\n","import { stripHtmlTags } from './stripHtmlTags'\n\n/**\n * Normalizes HTML into a lowercase, punctuation-free, single-spaced token string\n * for search indexing/matching. From alerts' cleanHTML; builds on stripHtmlTags\n * so tag removal stays single-sourced.\n * @param {string} html - The HTML string to clean.\n * @returns {string} The cleaned, normalized text.\n */\nexport const cleanHTML = (html: string): string =>\n stripHtmlTags(html)\n .toLowerCase()\n .replace(/[.,!?;:\"'()[\\]\\-_/\\\\]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n","import createDOMPurify from 'dompurify'\n\nimport type { SanitizeArticleHtmlOptions } from '../types/html/SanitizeArticleHtmlOptions'\n\n// Dedicated DOMPurify instance so the hardening hook below is scoped to this\n// sanitizer and never pollutes the consumer's shared default DOMPurify instance\n// (other code may sanitize through the default instance directly).\nconst purifier = createDOMPurify(window)\n\n// Set synchronously around each (synchronous) sanitize call. JS is\n// single-threaded, so the guard cannot leak across calls.\nlet activeIframeGuard: ((src: string) => boolean) | null = null\n\n// afterSanitizeAttributes hook: force rel=\"noopener noreferrer\" on\n// target=\"_blank\" anchors (reverse-tabnabbing), and drop iframes whose src fails\n// the injected allowlist predicate when one is active.\npurifier.addHook('afterSanitizeAttributes', (node) => {\n if (!(node instanceof Element)) return\n\n if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n node.setAttribute('rel', 'noopener noreferrer')\n }\n\n if (node.tagName === 'IFRAME' && activeIframeGuard) {\n const src = node.getAttribute('src') ?? ''\n if (!activeIframeGuard(src)) node.parentNode?.removeChild(node)\n }\n})\n\n/**\n * Canonical sanitizer for rich article HTML rendered into a session-bearing\n * webview. Superset of the alerts and unacknowledged-bulletins variants: keeps\n * iframes and data-* attributes (renderWidgets discovers embedded sub-widgets via\n * data-*), strips scripts/inline handlers/javascript:, and forces\n * rel=\"noopener noreferrer\" on target=\"_blank\" anchors. When options.isAllowedIframeSrc\n * is provided, iframes whose src fails it are dropped (injected so this module\n * does not depend on /links, which owns isAllowedIframeSrc).\n * @param {string} html - Raw article HTML from the Staffbase API.\n * @param {SanitizeArticleHtmlOptions} options - Optional iframe-src allowlist.\n * @returns {string} Sanitized HTML safe to inject/parse.\n */\nexport const sanitizeArticleHtml = (\n html: string,\n options: SanitizeArticleHtmlOptions = {},\n): string => {\n activeIframeGuard = options.isAllowedIframeSrc ?? null\n try {\n
|
|
1
|
+
{"version":3,"file":"html.es.mjs","names":[],"sources":["../src/html/stripHtmlTags.ts","../src/html/cleanHTML.ts","../src/html/customElementHandling.ts","../src/html/logRemovedEmbeds.ts","../src/html/sanitizeArticleHtml.ts","../src/html/sanitizeHtml.ts"],"sourcesContent":["import DOMPurify from 'dompurify'\n\n/**\n * Strips all HTML tags, keeping the text content. From smart-search's\n * stripHtmlTags; its html-react-parser fallback is intentionally dropped to\n * avoid a heavy runtime dependency — the DOMPurify path plus a regex fallback is\n * sufficient and never throws in practice.\n * @param {string | undefined} html - String that may contain HTML tags.\n * @returns {string} The text content with tags removed.\n */\nexport const stripHtmlTags = (html?: string): string => {\n if (!html) return ''\n\n try {\n return DOMPurify.sanitize(html, { ALLOWED_TAGS: [], KEEP_CONTENT: true })\n } catch {\n return html.replace(/<\\/?[^>]+(>|$)/g, '')\n }\n}\n","import { stripHtmlTags } from './stripHtmlTags'\n\n/**\n * Normalizes HTML into a lowercase, punctuation-free, single-spaced token string\n * for search indexing/matching. From alerts' cleanHTML; builds on stripHtmlTags\n * so tag removal stays single-sourced.\n * @param {string} html - The HTML string to clean.\n * @returns {string} The cleaned, normalized text.\n */\nexport const cleanHTML = (html: string): string =>\n stripHtmlTags(html)\n .toLowerCase()\n .replace(/[.,!?;:\"'()[\\]\\-_/\\\\]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n","import type { Config } from 'dompurify'\n\n/**\n * DOMPurify custom-element handling that preserves embedded-widget custom\n * elements (any hyphenated tag, e.g. `<news-teaser>`) and their kebab-case /\n * `data-` attributes so the host widget manager (renderWidgets) can hydrate\n * them. Without this, DOMPurify's default profile silently strips unknown custom\n * elements AND their attributes, wiping embeds out of the content before they can\n * render. `on*` event-handler attributes are still rejected (defense in depth\n * alongside DOMPurify's own XSS stripping). Promoted from staffbase-global-content.\n */\nexport const customElementHandling: NonNullable<\n Config['CUSTOM_ELEMENT_HANDLING']\n> = {\n // Any valid custom-element tag name (must contain a hyphen per the spec).\n tagNameCheck: /^[a-z][a-z0-9]*-[a-z0-9-]*$/,\n // Allow kebab-case / data- attributes on custom elements, but never `on*`.\n attributeNameCheck: /^(?!on)[a-z][a-z0-9]*([-:][a-z0-9]+)*$/,\n // Permit `<div is=\"some-widget\">`-style customized built-in elements.\n allowCustomizedBuiltInElements: true,\n}\n","/**\n * Warns (once per call) when the sanitizer dropped what looks like an embedded\n * widget — a custom-element tag (any hyphenated tag name). These removals are the\n * silent failure mode that makes embedded widgets \"disappear\" before the host can\n * render them, so surfacing them aids diagnosis. Scripts, styles and on*\n * handlers are intentionally removed and are NOT reported (they would be noise).\n * @param {readonly unknown[]} removed - DOMPurify's `removed` array after sanitize.\n * @returns {void}\n */\nexport const logRemovedEmbeds = (removed: readonly unknown[]): void => {\n const tags: string[] = []\n\n for (const entry of removed) {\n if (!entry || typeof entry !== 'object' || !('element' in entry)) continue\n const element = (entry as { element: unknown }).element\n if (element instanceof Element) {\n const tag = element.tagName.toLowerCase()\n if (tag.includes('-')) tags.push(tag)\n }\n }\n\n if (tags.length > 0) {\n console.warn(\n `[staffbase-utils] sanitizer removed ${tags.length} possible widget embed(s): ${tags.join(', ')} — they will not render. If these are valid embeds, the sanitizer config may need updating.`,\n )\n }\n}\n","import createDOMPurify from 'dompurify'\n\nimport type { SanitizeArticleHtmlOptions } from '../types/html/SanitizeArticleHtmlOptions'\nimport { customElementHandling } from './customElementHandling'\nimport { logRemovedEmbeds } from './logRemovedEmbeds'\n\n// Dedicated DOMPurify instance so the hardening hook below is scoped to this\n// sanitizer and never pollutes the consumer's shared default DOMPurify instance\n// (other code may sanitize through the default instance directly).\nconst purifier = createDOMPurify(window)\n\n// Set synchronously around each (synchronous) sanitize call. JS is\n// single-threaded, so the guard cannot leak across calls.\nlet activeIframeGuard: ((src: string) => boolean) | null = null\n\n// afterSanitizeAttributes hook: force rel=\"noopener noreferrer\" on\n// target=\"_blank\" anchors (reverse-tabnabbing), and drop iframes whose src fails\n// the injected allowlist predicate when one is active.\npurifier.addHook('afterSanitizeAttributes', (node) => {\n if (!(node instanceof Element)) return\n\n if (node.tagName === 'A' && node.getAttribute('target') === '_blank') {\n node.setAttribute('rel', 'noopener noreferrer')\n }\n\n if (node.tagName === 'IFRAME' && activeIframeGuard) {\n const src = node.getAttribute('src') ?? ''\n if (!activeIframeGuard(src)) node.parentNode?.removeChild(node)\n }\n})\n\n/**\n * Canonical sanitizer for rich article HTML rendered into a session-bearing\n * webview. Superset of the alerts and unacknowledged-bulletins variants: keeps\n * iframes and data-* attributes (renderWidgets discovers embedded sub-widgets via\n * data-*), strips scripts/inline handlers/javascript:, and forces\n * rel=\"noopener noreferrer\" on target=\"_blank\" anchors. When options.isAllowedIframeSrc\n * is provided, iframes whose src fails it are dropped (injected so this module\n * does not depend on /links, which owns isAllowedIframeSrc).\n * @param {string} html - Raw article HTML from the Staffbase API.\n * @param {SanitizeArticleHtmlOptions} options - Optional iframe-src allowlist.\n * @returns {string} Sanitized HTML safe to inject/parse.\n */\nexport const sanitizeArticleHtml = (\n html: string,\n options: SanitizeArticleHtmlOptions = {},\n): string => {\n activeIframeGuard = options.isAllowedIframeSrc ?? null\n try {\n const clean = purifier.sanitize(html, {\n USE_PROFILES: { html: true },\n CUSTOM_ELEMENT_HANDLING: customElementHandling,\n ADD_TAGS: ['iframe'],\n ADD_ATTR: [\n 'target',\n 'allow',\n 'allowfullscreen',\n 'frameborder',\n 'scrolling',\n 'loading',\n 'referrerpolicy',\n ],\n FORBID_TAGS: ['script', 'style'],\n FORBID_ATTR: ['onerror', 'onload', 'onclick'],\n })\n logRemovedEmbeds(purifier.removed)\n return clean\n } finally {\n activeIframeGuard = null\n }\n}\n","import DOMPurify from 'dompurify'\n\nimport { customElementHandling } from './customElementHandling'\nimport { logRemovedEmbeds } from './logRemovedEmbeds'\n\n/**\n * Strict sanitizer for untrusted snippet/teaser HTML rendered through a\n * non-sanitizing parser (e.g. html-react-parser). The default DOMPurify profile\n * strips scripts, inline handlers and dangerous URLs AND drops iframes, keeping\n * only basic rich-text markup. Embedded-widget custom elements (and their\n * kebab/data attributes) are preserved so the host can hydrate them. From\n * global-content's sanitizeHtml.\n *\n * Use sanitizeArticleHtml instead when rendering full article bodies that must\n * keep iframes / data-* embeds.\n * @param {string} html - Raw HTML string from the API.\n * @returns {string} Sanitized HTML.\n */\nexport const sanitizeHtml = (html: string): string => {\n const clean = DOMPurify.sanitize(html, {\n CUSTOM_ELEMENT_HANDLING: customElementHandling,\n })\n logRemovedEmbeds(DOMPurify.removed)\n return clean\n}\n"],"mappings":";;AAUA,IAAa,KAAiB,MAA0B;CACtD,IAAI,CAAC,GAAM,OAAO;CAElB,IAAI;EACF,OAAO,EAAU,SAAS,GAAM;GAAE,cAAc,CAAC;GAAG,cAAc;EAAK,CAAC;CAC1E,QAAQ;EACN,OAAO,EAAK,QAAQ,mBAAmB,EAAE;CAC3C;AACF,GCTa,KAAa,MACxB,EAAc,CAAI,EACf,YAAY,EACZ,QAAQ,0BAA0B,GAAG,EACrC,QAAQ,QAAQ,GAAG,EACnB,KAAK,GCHG,IAET;CAEF,cAAc;CAEd,oBAAoB;CAEpB,gCAAgC;AAClC,GCXa,KAAoB,MAAsC;CACrE,IAAM,IAAiB,CAAC;CAExB,KAAK,IAAM,KAAS,GAAS;EAC3B,IAAI,CAAC,KAAS,OAAO,KAAU,YAAY,EAAE,aAAa,IAAQ;EAClE,IAAM,IAAW,EAA+B;EAChD,IAAI,aAAmB,SAAS;GAC9B,IAAM,IAAM,EAAQ,QAAQ,YAAY;GACxC,AAAI,EAAI,SAAS,GAAG,KAAG,EAAK,KAAK,CAAG;EACtC;CACF;CAEA,AAAI,EAAK,SAAS,KAChB,QAAQ,KACN,uCAAuC,EAAK,OAAO,6BAA6B,EAAK,KAAK,IAAI,EAAE,4FAClG;AAEJ,GCjBM,IAAW,EAAgB,MAAM,GAInC,IAAuD;AAK3D,EAAS,QAAQ,4BAA4B,MAAS;CAC9C,iBAAgB,YAElB,EAAK,YAAY,OAAO,EAAK,aAAa,QAAQ,MAAM,YAC1D,EAAK,aAAa,OAAO,qBAAqB,GAG5C,EAAK,YAAY,YAAY,IAAmB;EAClD,IAAM,IAAM,EAAK,aAAa,KAAK,KAAK;EACxC,AAAK,EAAkB,CAAG,KAAG,EAAK,YAAY,YAAY,CAAI;CAChE;AACF,CAAC;AAcD,IAAa,KACX,GACA,IAAsC,CAAC,MAC5B;CACX,IAAoB,EAAQ,sBAAsB;CAClD,IAAI;EACF,IAAM,IAAQ,EAAS,SAAS,GAAM;GACpC,cAAc,EAAE,MAAM,GAAK;GAC3B,yBAAyB;GACzB,UAAU,CAAC,QAAQ;GACnB,UAAU;IACR;IACA;IACA;IACA;IACA;IACA;IACA;GACF;GACA,aAAa,CAAC,UAAU,OAAO;GAC/B,aAAa;IAAC;IAAW;IAAU;GAAS;EAC9C,CAAC;EAED,OADA,EAAiB,EAAS,OAAO,GAC1B;CACT,UAAU;EACR,IAAoB;CACtB;AACF,GCpDa,KAAgB,MAAyB;CACpD,IAAM,IAAQ,EAAU,SAAS,GAAM,EACrC,yBAAyB,EAC3B,CAAC;CAED,OADA,EAAiB,EAAU,OAAO,GAC3B;AACT"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Config } from 'dompurify';
|
|
2
|
+
/**
|
|
3
|
+
* DOMPurify custom-element handling that preserves embedded-widget custom
|
|
4
|
+
* elements (any hyphenated tag, e.g. `<news-teaser>`) and their kebab-case /
|
|
5
|
+
* `data-` attributes so the host widget manager (renderWidgets) can hydrate
|
|
6
|
+
* them. Without this, DOMPurify's default profile silently strips unknown custom
|
|
7
|
+
* elements AND their attributes, wiping embeds out of the content before they can
|
|
8
|
+
* render. `on*` event-handler attributes are still rejected (defense in depth
|
|
9
|
+
* alongside DOMPurify's own XSS stripping). Promoted from staffbase-global-content.
|
|
10
|
+
*/
|
|
11
|
+
export declare const customElementHandling: NonNullable<Config['CUSTOM_ELEMENT_HANDLING']>;
|
|
12
|
+
//# sourceMappingURL=customElementHandling.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"customElementHandling.d.ts","sourceRoot":"","sources":["../../../src/html/customElementHandling.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAEvC;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAC7C,MAAM,CAAC,yBAAyB,CAAC,CAQlC,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warns (once per call) when the sanitizer dropped what looks like an embedded
|
|
3
|
+
* widget — a custom-element tag (any hyphenated tag name). These removals are the
|
|
4
|
+
* silent failure mode that makes embedded widgets "disappear" before the host can
|
|
5
|
+
* render them, so surfacing them aids diagnosis. Scripts, styles and on*
|
|
6
|
+
* handlers are intentionally removed and are NOT reported (they would be noise).
|
|
7
|
+
* @param {readonly unknown[]} removed - DOMPurify's `removed` array after sanitize.
|
|
8
|
+
* @returns {void}
|
|
9
|
+
*/
|
|
10
|
+
export declare const logRemovedEmbeds: (removed: readonly unknown[]) => void;
|
|
11
|
+
//# sourceMappingURL=logRemovedEmbeds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logRemovedEmbeds.d.ts","sourceRoot":"","sources":["../../../src/html/logRemovedEmbeds.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,SAAS,OAAO,EAAE,KAAG,IAiB9D,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitizeArticleHtml.d.ts","sourceRoot":"","sources":["../../../src/html/sanitizeArticleHtml.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAA;
|
|
1
|
+
{"version":3,"file":"sanitizeArticleHtml.d.ts","sourceRoot":"","sources":["../../../src/html/sanitizeArticleHtml.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAA;AA6B1F;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,MAAM,EACZ,UAAS,0BAA+B,KACvC,MAwBF,CAAA"}
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Strict sanitizer for untrusted snippet/teaser HTML rendered through a
|
|
3
3
|
* non-sanitizing parser (e.g. html-react-parser). The default DOMPurify profile
|
|
4
4
|
* strips scripts, inline handlers and dangerous URLs AND drops iframes, keeping
|
|
5
|
-
* only basic rich-text markup.
|
|
5
|
+
* only basic rich-text markup. Embedded-widget custom elements (and their
|
|
6
|
+
* kebab/data attributes) are preserved so the host can hydrate them. From
|
|
7
|
+
* global-content's sanitizeHtml.
|
|
6
8
|
*
|
|
7
9
|
* Use sanitizeArticleHtml instead when rendering full article bodies that must
|
|
8
10
|
* keep iframes / data-* embeds.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitizeHtml.d.ts","sourceRoot":"","sources":["../../../src/html/sanitizeHtml.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sanitizeHtml.d.ts","sourceRoot":"","sources":["../../../src/html/sanitizeHtml.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,MAM3C,CAAA"}
|