{"version":3,"file":"html.es.mjs","names":[],"sources":["../src/html/cleanHTML.ts","../src/html/customElementHandling.ts","../src/html/logRemovedEmbeds.ts","../src/html/sanitizeArticleHtml.ts","../src/html/sanitizeHtml.ts"],"sourcesContent":["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":";;;AASA,IAAa,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"}