@favish/staffbase-utils 0.5.0 → 0.6.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 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),f=null;d.addHook(`afterSanitizeAttributes`,e=>{if(e instanceof Element&&(e.tagName===`A`&&e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`),e.tagName===`IFRAME`&&f)){let t=e.getAttribute(`src`)??``;f(t)||e.parentNode?.removeChild(e)}});var p=(e,t={})=>{f=t.isAllowedIframeSrc??null;try{return d.sanitize(e,{USE_PROFILES:{html:!0},ADD_TAGS:[`iframe`],ADD_ATTR:[`target`,`allow`,`allowfullscreen`,`frameborder`,`scrolling`,`loading`,`referrerpolicy`],FORBID_TAGS:[`script`,`style`],FORBID_ATTR:[`onerror`,`onload`,`onclick`]})}finally{f=null}},m=e=>c.default.sanitize(e);exports.cleanHTML=u,exports.sanitizeArticleHtml=p,exports.sanitizeHtml=m,exports.stripHtmlTags=l;
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=(0,c.default)(window),p=null;f.addHook(`afterSanitizeAttributes`,e=>{if(e instanceof Element&&(e.tagName===`A`&&e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`),e.tagName===`IFRAME`&&p)){let t=e.getAttribute(`src`)??``;p(t)||e.parentNode?.removeChild(e)}});var m=(e,t={})=>{p=t.isAllowedIframeSrc??null;try{return f.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`]})}finally{p=null}},h=e=>c.default.sanitize(e,{CUSTOM_ELEMENT_HANDLING:d});exports.cleanHTML=u,exports.sanitizeArticleHtml=m,exports.sanitizeHtml=h,exports.stripHtmlTags=l;
2
2
  //# sourceMappingURL=html.cjs.js.map
@@ -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 return purifier.sanitize(html, {\n USE_PROFILES: { html: true },\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 } finally {\n activeIframeGuard = null\n }\n}\n","import DOMPurify from 'dompurify'\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. From 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 => DOMPurify.sanitize(html)\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,ECPJ,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,OAAO,EAAS,SAAS,EAAM,CAC7B,aAAc,CAAE,KAAM,EAAK,EAC3B,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,CACH,QAAU,CACR,EAAoB,IACtB,CACF,ECpDa,EAAgB,GAAyB,EAAA,QAAU,SAAS,CAAI"}
1
+ {"version":3,"file":"html.cjs.js","names":[],"sources":["../src/html/stripHtmlTags.ts","../src/html/cleanHTML.ts","../src/html/customElementHandling.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","import createDOMPurify from 'dompurify'\n\nimport type { SanitizeArticleHtmlOptions } from '../types/html/SanitizeArticleHtmlOptions'\nimport { customElementHandling } from './customElementHandling'\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 return 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 } finally {\n activeIframeGuard = null\n }\n}\n","import DOMPurify from 'dompurify'\n\nimport { customElementHandling } from './customElementHandling'\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 DOMPurify.sanitize(html, { CUSTOM_ELEMENT_HANDLING: customElementHandling })\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,ECZM,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,OAAO,EAAS,SAAS,EAAM,CAC7B,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,CACH,QAAU,CACR,EAAoB,IACtB,CACF,EClDa,EAAgB,GAC3B,EAAA,QAAU,SAAS,EAAM,CAAE,wBAAyB,CAAsB,CAAC"}
package/dist/html.es.mjs CHANGED
@@ -10,18 +10,23 @@ 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 = e(window), i = null;
14
- r.addHook("afterSanitizeAttributes", (e) => {
15
- if (e instanceof Element && (e.tagName === "A" && e.getAttribute("target") === "_blank" && e.setAttribute("rel", "noopener noreferrer"), e.tagName === "IFRAME" && i)) {
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(window), a = null;
18
+ i.addHook("afterSanitizeAttributes", (e) => {
19
+ if (e instanceof Element && (e.tagName === "A" && e.getAttribute("target") === "_blank" && e.setAttribute("rel", "noopener noreferrer"), e.tagName === "IFRAME" && a)) {
16
20
  let t = e.getAttribute("src") ?? "";
17
- i(t) || e.parentNode?.removeChild(e);
21
+ a(t) || e.parentNode?.removeChild(e);
18
22
  }
19
23
  });
20
- var a = (e, t = {}) => {
21
- i = t.isAllowedIframeSrc ?? null;
24
+ var o = (e, t = {}) => {
25
+ a = t.isAllowedIframeSrc ?? null;
22
26
  try {
23
- return r.sanitize(e, {
27
+ return i.sanitize(e, {
24
28
  USE_PROFILES: { html: !0 },
29
+ CUSTOM_ELEMENT_HANDLING: r,
25
30
  ADD_TAGS: ["iframe"],
26
31
  ADD_ATTR: [
27
32
  "target",
@@ -40,10 +45,10 @@ var a = (e, t = {}) => {
40
45
  ]
41
46
  });
42
47
  } finally {
43
- i = null;
48
+ a = null;
44
49
  }
45
- }, o = (t) => e.sanitize(t);
50
+ }, s = (t) => e.sanitize(t, { CUSTOM_ELEMENT_HANDLING: r });
46
51
  //#endregion
47
- export { n as cleanHTML, a as sanitizeArticleHtml, o as sanitizeHtml, t as stripHtmlTags };
52
+ export { n as cleanHTML, o as sanitizeArticleHtml, s as sanitizeHtml, t as stripHtmlTags };
48
53
 
49
54
  //# sourceMappingURL=html.es.mjs.map
@@ -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 return purifier.sanitize(html, {\n USE_PROFILES: { html: true },\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 } finally {\n activeIframeGuard = null\n }\n}\n","import DOMPurify from 'dompurify'\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. From 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 => DOMPurify.sanitize(html)\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,GCPJ,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,OAAO,EAAS,SAAS,GAAM;GAC7B,cAAc,EAAE,MAAM,GAAK;GAC3B,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;CACH,UAAU;EACR,IAAoB;CACtB;AACF,GCpDa,KAAgB,MAAyB,EAAU,SAAS,CAAI"}
1
+ {"version":3,"file":"html.es.mjs","names":[],"sources":["../src/html/stripHtmlTags.ts","../src/html/cleanHTML.ts","../src/html/customElementHandling.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","import createDOMPurify from 'dompurify'\n\nimport type { SanitizeArticleHtmlOptions } from '../types/html/SanitizeArticleHtmlOptions'\nimport { customElementHandling } from './customElementHandling'\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 return 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 } finally {\n activeIframeGuard = null\n }\n}\n","import DOMPurify from 'dompurify'\n\nimport { customElementHandling } from './customElementHandling'\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 DOMPurify.sanitize(html, { CUSTOM_ELEMENT_HANDLING: customElementHandling })\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,GCZM,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,OAAO,EAAS,SAAS,GAAM;GAC7B,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;CACH,UAAU;EACR,IAAoB;CACtB;AACF,GClDa,KAAgB,MAC3B,EAAU,SAAS,GAAM,EAAE,yBAAyB,EAAsB,CAAC"}
@@ -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"}
@@ -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;AA2B1F;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,MAAM,EACZ,UAAS,0BAA+B,KACvC,MAqBF,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;AA4B1F;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAC9B,MAAM,MAAM,EACZ,UAAS,0BAA+B,KACvC,MAsBF,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. From global-content's sanitizeHtml.
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":"AAEA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,MAAkC,CAAA"}
1
+ {"version":3,"file":"sanitizeHtml.d.ts","sourceRoot":"","sources":["../../../src/html/sanitizeHtml.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,MACkC,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@favish/staffbase-utils",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Shared internal/host utilities for Staffbase widgets",
5
5
  "author": "Favish <dev@favish.com>",
6
6
  "license": "UNLICENSED",