UNPKG

@amplitude/rrweb-snapshot

Version:

rrweb's component to take a snapshot of DOM, aka DOM serializer

1 lines 323 kB
{"version":3,"file":"rrweb-snapshot.cjs","sources":["../../types/dist/rrweb-types.js","../../utils/dist/rrweb-utils.js","../src/utils.ts","../src/snapshot.ts","../src/css.ts","../../../node_modules/picocolors/picocolors.browser.js","../__vite-browser-external","../../../node_modules/postcss/lib/css-syntax-error.js","../../../node_modules/postcss/lib/symbols.js","../../../node_modules/postcss/lib/stringifier.js","../../../node_modules/postcss/lib/stringify.js","../../../node_modules/postcss/lib/node.js","../../../node_modules/postcss/lib/declaration.js","../../../node_modules/postcss/node_modules/nanoid/non-secure/index.cjs","../../../node_modules/postcss/lib/previous-map.js","../../../node_modules/postcss/lib/input.js","../../../node_modules/postcss/lib/map-generator.js","../../../node_modules/postcss/lib/comment.js","../../../node_modules/postcss/lib/container.js","../../../node_modules/postcss/lib/document.js","../../../node_modules/postcss/lib/warn-once.js","../../../node_modules/postcss/lib/warning.js","../../../node_modules/postcss/lib/result.js","../../../node_modules/postcss/lib/tokenize.js","../../../node_modules/postcss/lib/at-rule.js","../../../node_modules/postcss/lib/root.js","../../../node_modules/postcss/lib/list.js","../../../node_modules/postcss/lib/rule.js","../../../node_modules/postcss/lib/parser.js","../../../node_modules/postcss/lib/parse.js","../../../node_modules/postcss/lib/lazy-result.js","../../../node_modules/postcss/lib/no-work-result.js","../../../node_modules/postcss/lib/processor.js","../../../node_modules/postcss/lib/fromJSON.js","../../../node_modules/postcss/lib/postcss.js","../../../node_modules/postcss/lib/postcss.mjs","../src/rebuild.ts"],"sourcesContent":["var EventType = /* @__PURE__ */ ((EventType2) => {\n EventType2[EventType2[\"DomContentLoaded\"] = 0] = \"DomContentLoaded\";\n EventType2[EventType2[\"Load\"] = 1] = \"Load\";\n EventType2[EventType2[\"FullSnapshot\"] = 2] = \"FullSnapshot\";\n EventType2[EventType2[\"IncrementalSnapshot\"] = 3] = \"IncrementalSnapshot\";\n EventType2[EventType2[\"Meta\"] = 4] = \"Meta\";\n EventType2[EventType2[\"Custom\"] = 5] = \"Custom\";\n EventType2[EventType2[\"Plugin\"] = 6] = \"Plugin\";\n return EventType2;\n})(EventType || {});\nvar IncrementalSource = /* @__PURE__ */ ((IncrementalSource2) => {\n IncrementalSource2[IncrementalSource2[\"Mutation\"] = 0] = \"Mutation\";\n IncrementalSource2[IncrementalSource2[\"MouseMove\"] = 1] = \"MouseMove\";\n IncrementalSource2[IncrementalSource2[\"MouseInteraction\"] = 2] = \"MouseInteraction\";\n IncrementalSource2[IncrementalSource2[\"Scroll\"] = 3] = \"Scroll\";\n IncrementalSource2[IncrementalSource2[\"ViewportResize\"] = 4] = \"ViewportResize\";\n IncrementalSource2[IncrementalSource2[\"Input\"] = 5] = \"Input\";\n IncrementalSource2[IncrementalSource2[\"TouchMove\"] = 6] = \"TouchMove\";\n IncrementalSource2[IncrementalSource2[\"MediaInteraction\"] = 7] = \"MediaInteraction\";\n IncrementalSource2[IncrementalSource2[\"StyleSheetRule\"] = 8] = \"StyleSheetRule\";\n IncrementalSource2[IncrementalSource2[\"CanvasMutation\"] = 9] = \"CanvasMutation\";\n IncrementalSource2[IncrementalSource2[\"Font\"] = 10] = \"Font\";\n IncrementalSource2[IncrementalSource2[\"Log\"] = 11] = \"Log\";\n IncrementalSource2[IncrementalSource2[\"Drag\"] = 12] = \"Drag\";\n IncrementalSource2[IncrementalSource2[\"StyleDeclaration\"] = 13] = \"StyleDeclaration\";\n IncrementalSource2[IncrementalSource2[\"Selection\"] = 14] = \"Selection\";\n IncrementalSource2[IncrementalSource2[\"AdoptedStyleSheet\"] = 15] = \"AdoptedStyleSheet\";\n IncrementalSource2[IncrementalSource2[\"CustomElement\"] = 16] = \"CustomElement\";\n return IncrementalSource2;\n})(IncrementalSource || {});\nvar MouseInteractions = /* @__PURE__ */ ((MouseInteractions2) => {\n MouseInteractions2[MouseInteractions2[\"MouseUp\"] = 0] = \"MouseUp\";\n MouseInteractions2[MouseInteractions2[\"MouseDown\"] = 1] = \"MouseDown\";\n MouseInteractions2[MouseInteractions2[\"Click\"] = 2] = \"Click\";\n MouseInteractions2[MouseInteractions2[\"ContextMenu\"] = 3] = \"ContextMenu\";\n MouseInteractions2[MouseInteractions2[\"DblClick\"] = 4] = \"DblClick\";\n MouseInteractions2[MouseInteractions2[\"Focus\"] = 5] = \"Focus\";\n MouseInteractions2[MouseInteractions2[\"Blur\"] = 6] = \"Blur\";\n MouseInteractions2[MouseInteractions2[\"TouchStart\"] = 7] = \"TouchStart\";\n MouseInteractions2[MouseInteractions2[\"TouchMove_Departed\"] = 8] = \"TouchMove_Departed\";\n MouseInteractions2[MouseInteractions2[\"TouchEnd\"] = 9] = \"TouchEnd\";\n MouseInteractions2[MouseInteractions2[\"TouchCancel\"] = 10] = \"TouchCancel\";\n return MouseInteractions2;\n})(MouseInteractions || {});\nvar PointerTypes = /* @__PURE__ */ ((PointerTypes2) => {\n PointerTypes2[PointerTypes2[\"Mouse\"] = 0] = \"Mouse\";\n PointerTypes2[PointerTypes2[\"Pen\"] = 1] = \"Pen\";\n PointerTypes2[PointerTypes2[\"Touch\"] = 2] = \"Touch\";\n return PointerTypes2;\n})(PointerTypes || {});\nvar CanvasContext = /* @__PURE__ */ ((CanvasContext2) => {\n CanvasContext2[CanvasContext2[\"2D\"] = 0] = \"2D\";\n CanvasContext2[CanvasContext2[\"WebGL\"] = 1] = \"WebGL\";\n CanvasContext2[CanvasContext2[\"WebGL2\"] = 2] = \"WebGL2\";\n return CanvasContext2;\n})(CanvasContext || {});\nvar MediaInteractions = /* @__PURE__ */ ((MediaInteractions2) => {\n MediaInteractions2[MediaInteractions2[\"Play\"] = 0] = \"Play\";\n MediaInteractions2[MediaInteractions2[\"Pause\"] = 1] = \"Pause\";\n MediaInteractions2[MediaInteractions2[\"Seeked\"] = 2] = \"Seeked\";\n MediaInteractions2[MediaInteractions2[\"VolumeChange\"] = 3] = \"VolumeChange\";\n MediaInteractions2[MediaInteractions2[\"RateChange\"] = 4] = \"RateChange\";\n return MediaInteractions2;\n})(MediaInteractions || {});\nvar ReplayerEvents = /* @__PURE__ */ ((ReplayerEvents2) => {\n ReplayerEvents2[\"Start\"] = \"start\";\n ReplayerEvents2[\"Pause\"] = \"pause\";\n ReplayerEvents2[\"Resume\"] = \"resume\";\n ReplayerEvents2[\"Resize\"] = \"resize\";\n ReplayerEvents2[\"Finish\"] = \"finish\";\n ReplayerEvents2[\"FullsnapshotRebuilded\"] = \"fullsnapshot-rebuilded\";\n ReplayerEvents2[\"LoadStylesheetStart\"] = \"load-stylesheet-start\";\n ReplayerEvents2[\"LoadStylesheetEnd\"] = \"load-stylesheet-end\";\n ReplayerEvents2[\"SkipStart\"] = \"skip-start\";\n ReplayerEvents2[\"SkipEnd\"] = \"skip-end\";\n ReplayerEvents2[\"MouseInteraction\"] = \"mouse-interaction\";\n ReplayerEvents2[\"EventCast\"] = \"event-cast\";\n ReplayerEvents2[\"CustomEvent\"] = \"custom-event\";\n ReplayerEvents2[\"Flush\"] = \"flush\";\n ReplayerEvents2[\"StateChange\"] = \"state-change\";\n ReplayerEvents2[\"PlayBack\"] = \"play-back\";\n ReplayerEvents2[\"Destroy\"] = \"destroy\";\n return ReplayerEvents2;\n})(ReplayerEvents || {});\nvar NodeType = /* @__PURE__ */ ((NodeType2) => {\n NodeType2[NodeType2[\"Document\"] = 0] = \"Document\";\n NodeType2[NodeType2[\"DocumentType\"] = 1] = \"DocumentType\";\n NodeType2[NodeType2[\"Element\"] = 2] = \"Element\";\n NodeType2[NodeType2[\"Text\"] = 3] = \"Text\";\n NodeType2[NodeType2[\"CDATA\"] = 4] = \"CDATA\";\n NodeType2[NodeType2[\"Comment\"] = 5] = \"Comment\";\n return NodeType2;\n})(NodeType || {});\nexport {\n CanvasContext,\n EventType,\n IncrementalSource,\n MediaInteractions,\n MouseInteractions,\n NodeType,\n PointerTypes,\n ReplayerEvents\n};\n//# sourceMappingURL=rrweb-types.js.map\n","const testableAccessors = {\n Node: [\"childNodes\", \"parentNode\", \"parentElement\", \"textContent\"],\n ShadowRoot: [\"host\", \"styleSheets\"],\n Element: [\"shadowRoot\", \"querySelector\", \"querySelectorAll\"],\n MutationObserver: []\n};\nconst testableMethods = {\n Node: [\"contains\", \"getRootNode\"],\n ShadowRoot: [\"getSelection\"],\n Element: [],\n MutationObserver: [\"constructor\"]\n};\nconst untaintedBasePrototype = {};\nfunction angularZoneUnpatchedAlternative(key) {\n var _a, _b;\n const angularUnpatchedVersionSymbol = (_b = (_a = globalThis == null ? void 0 : globalThis.Zone) == null ? void 0 : _a.__symbol__) == null ? void 0 : _b.call(_a, key);\n if (angularUnpatchedVersionSymbol && globalThis[angularUnpatchedVersionSymbol]) {\n return globalThis[angularUnpatchedVersionSymbol];\n } else {\n return void 0;\n }\n}\nfunction getUntaintedPrototype(key) {\n if (untaintedBasePrototype[key])\n return untaintedBasePrototype[key];\n const candidate = angularZoneUnpatchedAlternative(key) || globalThis[key];\n const defaultPrototype = candidate.prototype;\n const accessorNames = key in testableAccessors ? testableAccessors[key] : void 0;\n const isUntaintedAccessors = Boolean(\n accessorNames && // @ts-expect-error 2345\n accessorNames.every(\n (accessor) => {\n var _a, _b;\n return Boolean(\n (_b = (_a = Object.getOwnPropertyDescriptor(defaultPrototype, accessor)) == null ? void 0 : _a.get) == null ? void 0 : _b.toString().includes(\"[native code]\")\n );\n }\n )\n );\n const methodNames = key in testableMethods ? testableMethods[key] : void 0;\n const isUntaintedMethods = Boolean(\n methodNames && methodNames.every(\n // @ts-expect-error 2345\n (method) => {\n var _a;\n return typeof defaultPrototype[method] === \"function\" && ((_a = defaultPrototype[method]) == null ? void 0 : _a.toString().includes(\"[native code]\"));\n }\n )\n );\n if (isUntaintedAccessors && isUntaintedMethods) {\n untaintedBasePrototype[key] = candidate.prototype;\n return candidate.prototype;\n }\n try {\n const iframeEl = document.createElement(\"iframe\");\n document.body.appendChild(iframeEl);\n const win = iframeEl.contentWindow;\n if (!win) return candidate.prototype;\n const untaintedObject = win[key].prototype;\n document.body.removeChild(iframeEl);\n if (!untaintedObject) return defaultPrototype;\n return untaintedBasePrototype[key] = untaintedObject;\n } catch {\n return defaultPrototype;\n }\n}\nconst untaintedAccessorCache = {};\nfunction getUntaintedAccessor(key, instance, accessor) {\n var _a;\n const cacheKey = `${key}.${String(accessor)}`;\n if (untaintedAccessorCache[cacheKey])\n return untaintedAccessorCache[cacheKey].call(\n instance\n );\n const untaintedPrototype = getUntaintedPrototype(key);\n const untaintedAccessor = (_a = Object.getOwnPropertyDescriptor(\n untaintedPrototype,\n accessor\n )) == null ? void 0 : _a.get;\n if (!untaintedAccessor) return instance[accessor];\n untaintedAccessorCache[cacheKey] = untaintedAccessor;\n return untaintedAccessor.call(instance);\n}\nconst untaintedMethodCache = {};\nfunction getUntaintedMethod(key, instance, method) {\n const cacheKey = `${key}.${String(method)}`;\n if (untaintedMethodCache[cacheKey])\n return untaintedMethodCache[cacheKey].bind(\n instance\n );\n const untaintedPrototype = getUntaintedPrototype(key);\n const untaintedMethod = untaintedPrototype[method];\n if (typeof untaintedMethod !== \"function\") return instance[method];\n untaintedMethodCache[cacheKey] = untaintedMethod;\n return untaintedMethod.bind(instance);\n}\nfunction childNodes(n) {\n return getUntaintedAccessor(\"Node\", n, \"childNodes\");\n}\nfunction parentNode(n) {\n return getUntaintedAccessor(\"Node\", n, \"parentNode\");\n}\nfunction parentElement(n) {\n return getUntaintedAccessor(\"Node\", n, \"parentElement\");\n}\nfunction textContent(n) {\n return getUntaintedAccessor(\"Node\", n, \"textContent\");\n}\nfunction contains(n, other) {\n return getUntaintedMethod(\"Node\", n, \"contains\")(other);\n}\nfunction getRootNode(n) {\n return getUntaintedMethod(\"Node\", n, \"getRootNode\")();\n}\nfunction host(n) {\n if (!n || !(\"host\" in n)) return null;\n return getUntaintedAccessor(\"ShadowRoot\", n, \"host\");\n}\nfunction styleSheets(n) {\n return n.styleSheets;\n}\nfunction shadowRoot(n) {\n if (!n || !(\"shadowRoot\" in n)) return null;\n return getUntaintedAccessor(\"Element\", n, \"shadowRoot\");\n}\nfunction querySelector(n, selectors) {\n return getUntaintedAccessor(\"Element\", n, \"querySelector\")(selectors);\n}\nfunction querySelectorAll(n, selectors) {\n return getUntaintedAccessor(\"Element\", n, \"querySelectorAll\")(selectors);\n}\nfunction mutationObserverCtor() {\n return getUntaintedPrototype(\"MutationObserver\").constructor;\n}\nconst index = {\n childNodes,\n parentNode,\n parentElement,\n textContent,\n contains,\n getRootNode,\n host,\n styleSheets,\n shadowRoot,\n querySelector,\n querySelectorAll,\n mutationObserver: mutationObserverCtor\n};\nexport {\n childNodes,\n contains,\n index as default,\n getRootNode,\n getUntaintedAccessor,\n getUntaintedMethod,\n getUntaintedPrototype,\n host,\n mutationObserverCtor,\n parentElement,\n parentNode,\n querySelector,\n querySelectorAll,\n shadowRoot,\n styleSheets,\n textContent\n};\n//# sourceMappingURL=rrweb-utils.js.map\n","import type {\n idNodeMap,\n MaskInputFn,\n MaskInputOptions,\n nodeMetaMap,\n} from './types';\n\nimport { NodeType } from '@amplitude/rrweb-types';\nimport type {\n IMirror,\n serializedNodeWithId,\n serializedNode,\n documentNode,\n documentTypeNode,\n textNode,\n elementNode,\n} from '@amplitude/rrweb-types';\nimport dom from '@amplitude/rrweb-utils';\n\nexport function isElement(n: Node): n is Element {\n return n.nodeType === n.ELEMENT_NODE;\n}\n\nexport function isShadowRoot(n: Node): n is ShadowRoot {\n const hostEl: Element | null =\n // anchor and textarea elements also have a `host` property\n // but only shadow roots have a `mode` property\n (n && 'host' in n && 'mode' in n && dom.host(n as ShadowRoot)) || null;\n return Boolean(\n hostEl && 'shadowRoot' in hostEl && dom.shadowRoot(hostEl) === n,\n );\n}\n\n/**\n * To fix the issue https://github.com/rrweb-io/rrweb/issues/933.\n * Some websites use polyfilled shadow dom and this function is used to detect this situation.\n */\nexport function isNativeShadowDom(shadowRoot: ShadowRoot): boolean {\n return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]';\n}\n\n/**\n * Browsers sometimes destructively modify the css rules they receive.\n * This function tries to rectify the modifications the browser made to make it more cross platform compatible.\n * @param cssText - output of `CSSStyleRule.cssText`\n * @returns `cssText` with browser inconsistencies fixed.\n */\nfunction fixBrowserCompatibilityIssuesInCSS(cssText: string): string {\n /**\n * Chrome outputs `-webkit-background-clip` as `background-clip` in `CSSStyleRule.cssText`.\n * But then Chrome ignores `background-clip` as css input.\n * Re-introduce `-webkit-background-clip` to fix this issue.\n */\n if (\n cssText.includes(' background-clip: text;') &&\n !cssText.includes(' -webkit-background-clip: text;')\n ) {\n cssText = cssText.replace(\n /\\sbackground-clip:\\s*text;/g,\n ' -webkit-background-clip: text; background-clip: text;',\n );\n }\n return cssText;\n}\n\n// Remove this declaration once typescript has added `CSSImportRule.supportsText` to the lib.\ndeclare interface CSSImportRule extends CSSRule {\n readonly href: string;\n readonly layerName: string | null;\n readonly media: MediaList;\n readonly styleSheet: CSSStyleSheet;\n /**\n * experimental API, currently only supported in firefox\n * https://developer.mozilla.org/en-US/docs/Web/API/CSSImportRule/supportsText\n */\n readonly supportsText?: string | null;\n}\n\n/**\n * Browsers sometimes incorrectly escape `@import` on `.cssText` statements.\n * This function tries to correct the escaping.\n * more info: https://bugs.chromium.org/p/chromium/issues/detail?id=1472259\n * @param cssImportRule\n * @returns `cssText` with browser inconsistencies fixed, or null if not applicable.\n */\nexport function escapeImportStatement(rule: CSSImportRule): string {\n const { cssText } = rule;\n if (cssText.split('\"').length < 3) return cssText;\n\n const statement = ['@import', `url(${JSON.stringify(rule.href)})`];\n if (rule.layerName === '') {\n statement.push(`layer`);\n } else if (rule.layerName) {\n statement.push(`layer(${rule.layerName})`);\n }\n if (rule.supportsText) {\n statement.push(`supports(${rule.supportsText})`);\n }\n if (rule.media.length) {\n statement.push(rule.media.mediaText);\n }\n return statement.join(' ') + ';';\n}\n\n/*\n * serialize the css rules from the .sheet property\n * for <link rel=\"stylesheet\"> elements, this is the only way of getting the rules without a FETCH\n * for <style> elements, this is less preferable to looking at childNodes[0].textContent\n * (which will include vendor prefixed rules which may not be used or visible to the recorded browser,\n * but which might be needed by the replayer browser)\n * however, at snapshot time, we don't know whether the style element has suffered\n * any programmatic manipulation prior to the snapshot, in which case the .sheet would be more up to date\n */\nexport function stringifyStylesheet(s: CSSStyleSheet): string | null {\n try {\n const rules = s.rules || s.cssRules;\n if (!rules) {\n return null;\n }\n let sheetHref = s.href;\n if (!sheetHref && s.ownerNode && s.ownerNode.ownerDocument) {\n // an inline <style> element\n sheetHref = s.ownerNode.ownerDocument.location.href;\n }\n const stringifiedRules = Array.from(rules, (rule: CSSRule) =>\n stringifyRule(rule, sheetHref),\n ).join('');\n return fixBrowserCompatibilityIssuesInCSS(stringifiedRules);\n } catch (error) {\n return null;\n }\n}\n\nexport function stringifyRule(rule: CSSRule, sheetHref: string | null): string {\n if (isCSSImportRule(rule)) {\n let importStringified;\n try {\n importStringified =\n // for same-origin stylesheets,\n // we can access the imported stylesheet rules directly\n stringifyStylesheet(rule.styleSheet) ||\n // work around browser issues with the raw string `@import url(...)` statement\n escapeImportStatement(rule);\n } catch (error) {\n importStringified = rule.cssText;\n }\n if (rule.styleSheet.href) {\n // url()s within the imported stylesheet are relative to _that_ sheet's href\n return absolutifyURLs(importStringified, rule.styleSheet.href);\n }\n return importStringified;\n } else {\n let ruleStringified = rule.cssText;\n if (isCSSStyleRule(rule) && rule.selectorText.includes(':')) {\n // Safari does not escape selectors with : properly\n // see https://bugs.webkit.org/show_bug.cgi?id=184604\n ruleStringified = fixSafariColons(ruleStringified);\n }\n if (sheetHref) {\n return absolutifyURLs(ruleStringified, sheetHref);\n }\n return ruleStringified;\n }\n}\n\nexport function fixSafariColons(cssStringified: string): string {\n // Replace e.g. [aa:bb] with [aa\\\\:bb]\n const regex = /(\\[(?:[\\w-]+)[^\\\\])(:(?:[\\w-]+)\\])/gm;\n return cssStringified.replace(regex, '$1\\\\$2');\n}\n\nexport function isCSSImportRule(rule: CSSRule): rule is CSSImportRule {\n return 'styleSheet' in rule;\n}\n\nexport function isCSSStyleRule(rule: CSSRule): rule is CSSStyleRule {\n return 'selectorText' in rule;\n}\n\nexport class Mirror implements IMirror<Node> {\n private idNodeMap: idNodeMap = new Map();\n private nodeMetaMap: nodeMetaMap = new WeakMap();\n\n getId(n: Node | undefined | null): number {\n if (!n) return -1;\n\n const id = this.getMeta(n)?.id;\n\n // if n is not a serialized Node, use -1 as its id.\n return id ?? -1;\n }\n\n getNode(id: number): Node | null {\n return this.idNodeMap.get(id) || null;\n }\n\n getIds(): number[] {\n return Array.from(this.idNodeMap.keys());\n }\n\n getMeta(n: Node): serializedNodeWithId | null {\n return this.nodeMetaMap.get(n) || null;\n }\n\n // removes the node from idNodeMap\n // doesn't remove the node from nodeMetaMap\n removeNodeFromMap(n: Node) {\n const id = this.getId(n);\n this.idNodeMap.delete(id);\n\n if (n.childNodes) {\n n.childNodes.forEach((childNode) =>\n this.removeNodeFromMap(childNode as unknown as Node),\n );\n }\n }\n has(id: number): boolean {\n return this.idNodeMap.has(id);\n }\n\n hasNode(node: Node): boolean {\n return this.nodeMetaMap.has(node);\n }\n\n add(n: Node, meta: serializedNodeWithId) {\n const id = meta.id;\n this.idNodeMap.set(id, n);\n this.nodeMetaMap.set(n, meta);\n }\n\n replace(id: number, n: Node) {\n const oldNode = this.getNode(id);\n if (oldNode) {\n const meta = this.nodeMetaMap.get(oldNode);\n if (meta) this.nodeMetaMap.set(n, meta);\n }\n this.idNodeMap.set(id, n);\n }\n\n reset() {\n this.idNodeMap = new Map();\n this.nodeMetaMap = new WeakMap();\n }\n}\n\nexport function createMirror(): Mirror {\n return new Mirror();\n}\n\nexport function maskInputValue({\n element,\n maskInputOptions,\n tagName,\n type,\n value,\n maskInputFn,\n}: {\n element: HTMLElement;\n maskInputOptions: MaskInputOptions;\n tagName: string;\n type: string | null;\n value: string | null;\n maskInputFn?: MaskInputFn;\n}): string {\n let text = value || '';\n const actualType = type && toLowerCase(type);\n\n if (\n maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] ||\n (actualType && maskInputOptions[actualType as keyof MaskInputOptions])\n ) {\n if (maskInputFn) {\n text = maskInputFn(text, element);\n } else {\n text = '*'.repeat(text.length);\n }\n }\n return text;\n}\n\nexport function toLowerCase<T extends string>(str: T): Lowercase<T> {\n return str.toLowerCase() as unknown as Lowercase<T>;\n}\n\nconst ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__';\ntype PatchedGetImageData = {\n [ORIGINAL_ATTRIBUTE_NAME]: CanvasImageData['getImageData'];\n} & CanvasImageData['getImageData'];\n\nexport function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean {\n const ctx = canvas.getContext('2d');\n if (!ctx) return true;\n\n const chunkSize = 50;\n\n // get chunks of the canvas and check if it is blank\n for (let x = 0; x < canvas.width; x += chunkSize) {\n for (let y = 0; y < canvas.height; y += chunkSize) {\n // eslint-disable-next-line @typescript-eslint/unbound-method\n const getImageData = ctx.getImageData as PatchedGetImageData;\n const originalGetImageData =\n ORIGINAL_ATTRIBUTE_NAME in getImageData\n ? getImageData[ORIGINAL_ATTRIBUTE_NAME]\n : getImageData;\n // by getting the canvas in chunks we avoid an expensive\n // `getImageData` call that retrieves everything\n // even if we can already tell from the first chunk(s) that\n // the canvas isn't blank\n const pixelBuffer = new Uint32Array(\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access\n originalGetImageData.call(\n ctx,\n x,\n y,\n Math.min(chunkSize, canvas.width - x),\n Math.min(chunkSize, canvas.height - y),\n ).data.buffer,\n );\n if (pixelBuffer.some((pixel) => pixel !== 0)) return false;\n }\n }\n return true;\n}\n\nexport function isNodeMetaEqual(a: serializedNode, b: serializedNode): boolean {\n if (!a || !b || a.type !== b.type) return false;\n if (a.type === NodeType.Document)\n return a.compatMode === (b as documentNode).compatMode;\n else if (a.type === NodeType.DocumentType)\n return (\n a.name === (b as documentTypeNode).name &&\n a.publicId === (b as documentTypeNode).publicId &&\n a.systemId === (b as documentTypeNode).systemId\n );\n else if (\n a.type === NodeType.Comment ||\n a.type === NodeType.Text ||\n a.type === NodeType.CDATA\n )\n return a.textContent === (b as textNode).textContent;\n else if (a.type === NodeType.Element)\n return (\n a.tagName === (b as elementNode).tagName &&\n JSON.stringify(a.attributes) ===\n JSON.stringify((b as elementNode).attributes) &&\n a.isSVG === (b as elementNode).isSVG &&\n a.needBlock === (b as elementNode).needBlock\n );\n return false;\n}\n\n/**\n * Get the type of an input element.\n * This takes care of the case where a password input is changed to a text input.\n * In this case, we continue to consider this of type password, in order to avoid leaking sensitive data\n * where passwords should be masked.\n */\nexport function getInputType(element: HTMLElement): Lowercase<string> | null {\n // when omitting the type of input element(e.g. <input />), the type is treated as text\n const type = (element as HTMLInputElement).type;\n\n return element.hasAttribute('data-rr-is-password')\n ? 'password'\n : type\n ? // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion\n toLowerCase(type)\n : null;\n}\n\n/**\n * Extracts the file extension from an a path, considering search parameters and fragments.\n * @param path - Path to file\n * @param baseURL - [optional] Base URL of the page, used to resolve relative paths. Defaults to current page URL.\n */\nexport function extractFileExtension(\n path: string,\n baseURL?: string,\n): string | null {\n let url;\n try {\n url = new URL(path, baseURL ?? window.location.href);\n } catch (err) {\n return null;\n }\n const regex = /\\.([0-9a-z]+)(?:$)/i;\n const match = url.pathname.match(regex);\n return match?.[1] ?? null;\n}\n\nfunction extractOrigin(url: string): string {\n let origin = '';\n if (url.indexOf('//') > -1) {\n origin = url.split('/').slice(0, 3).join('/');\n } else {\n origin = url.split('/')[0];\n }\n origin = origin.split('?')[0];\n return origin;\n}\n\nconst URL_IN_CSS_REF = /url\\((?:(')([^']*)'|(\")(.*?)\"|([^)]*))\\)/gm;\nconst URL_PROTOCOL_MATCH = /^(?:[a-z+]+:)?\\/\\//i;\nconst URL_WWW_MATCH = /^www\\..*/i;\nconst DATA_URI = /^(data:)([^,]*),(.*)/i;\nexport function absolutifyURLs(cssText: string | null, href: string): string {\n return (cssText || '').replace(\n URL_IN_CSS_REF,\n (\n origin: string,\n quote1: string,\n path1: string,\n quote2: string,\n path2: string,\n path3: string,\n ) => {\n const filePath = path1 || path2 || path3;\n const maybeQuote = quote1 || quote2 || '';\n if (!filePath) {\n return origin;\n }\n if (URL_PROTOCOL_MATCH.test(filePath) || URL_WWW_MATCH.test(filePath)) {\n return `url(${maybeQuote}${filePath}${maybeQuote})`;\n }\n if (DATA_URI.test(filePath)) {\n return `url(${maybeQuote}${filePath}${maybeQuote})`;\n }\n if (filePath[0] === '/') {\n return `url(${maybeQuote}${\n extractOrigin(href) + filePath\n }${maybeQuote})`;\n }\n const stack = href.split('/');\n const parts = filePath.split('/');\n stack.pop();\n for (const part of parts) {\n if (part === '.') {\n continue;\n } else if (part === '..') {\n stack.pop();\n } else {\n stack.push(part);\n }\n }\n return `url(${maybeQuote}${stack.join('/')}${maybeQuote})`;\n },\n );\n}\n\n/**\n * Intention is to normalize by remove spaces, semicolons and CSS comments\n * so that we can compare css as authored vs. output of stringifyStylesheet\n */\nexport function normalizeCssString(\n cssText: string,\n /**\n * _testNoPxNorm: only used as part of the 'substring matching going from many to none'\n * test case so that it will trigger a failure if the conditions that let to the creation of that test arise again\n */\n _testNoPxNorm = false,\n): string {\n if (_testNoPxNorm) {\n return cssText.replace(/(\\/\\*[^*]*\\*\\/)|[\\s;]/g, '');\n } else {\n return cssText.replace(/(\\/\\*[^*]*\\*\\/)|[\\s;]/g, '').replace(/0px/g, '0');\n }\n}\n\n/**\n * Maps the output of stringifyStylesheet to individual text nodes of a <style> element\n * which occurs when javascript is used to append to the style element\n * and may also occur when browsers opt to break up large text nodes\n * performance needs to be considered, see e.g. #1603\n */\nexport function splitCssText(\n cssText: string,\n style: HTMLStyleElement,\n _testNoPxNorm = false,\n): string[] {\n const childNodes = Array.from(style.childNodes);\n const splits: string[] = [];\n let iterCount = 0;\n if (childNodes.length > 1 && cssText && typeof cssText === 'string') {\n let cssTextNorm = normalizeCssString(cssText, _testNoPxNorm);\n const normFactor = cssTextNorm.length / cssText.length;\n for (let i = 1; i < childNodes.length; i++) {\n if (\n childNodes[i].textContent &&\n typeof childNodes[i].textContent === 'string'\n ) {\n const textContentNorm = normalizeCssString(\n childNodes[i].textContent!,\n _testNoPxNorm,\n );\n const jLimit = 100; // how many iterations for the first part of searching\n let j = 3;\n for (; j < textContentNorm.length; j++) {\n if (\n // keep consuming css identifiers (to get a decent chunk more quickly)\n textContentNorm[j].match(/[a-zA-Z0-9]/) ||\n // substring needs to be unique to this section\n textContentNorm.indexOf(textContentNorm.substring(0, j), 1) !== -1\n ) {\n continue;\n }\n break;\n }\n for (; j < textContentNorm.length; j++) {\n let startSubstring = textContentNorm.substring(0, j);\n // this substring should appears only once in overall text too\n let cssNormSplits = cssTextNorm.split(startSubstring);\n let splitNorm = -1;\n if (cssNormSplits.length === 2) {\n splitNorm = cssNormSplits[0].length;\n } else if (\n cssNormSplits.length > 2 &&\n cssNormSplits[0] === '' &&\n childNodes[i - 1].textContent !== ''\n ) {\n // this childNode has same starting content as previous\n splitNorm = cssTextNorm.indexOf(startSubstring, 1);\n } else if (cssNormSplits.length === 1) {\n // try to roll back to get multiple matches again\n startSubstring = startSubstring.substring(\n 0,\n startSubstring.length - 1,\n );\n cssNormSplits = cssTextNorm.split(startSubstring);\n if (cssNormSplits.length <= 1) {\n // no split possible\n splits.push(cssText);\n return splits;\n }\n j = jLimit + 1; // trigger end of search\n } else if (j === textContentNorm.length - 1) {\n // we're about to end loop without a split point\n splitNorm = cssTextNorm.indexOf(startSubstring);\n }\n if (cssNormSplits.length >= 2 && j > jLimit) {\n const prevTextContent = childNodes[i - 1].textContent;\n if (prevTextContent && typeof prevTextContent === 'string') {\n // pick the first matching point which respects the previous chunk's approx size\n const prevMinLength = normalizeCssString(prevTextContent).length;\n splitNorm = cssTextNorm.indexOf(startSubstring, prevMinLength);\n }\n if (splitNorm === -1) {\n // fall back to pick the first matching point of many\n splitNorm = cssNormSplits[0].length;\n }\n }\n if (splitNorm !== -1) {\n // find the split point in the original text\n let k = Math.floor(splitNorm / normFactor);\n for (; k > 0 && k < cssText.length; ) {\n iterCount += 1;\n if (iterCount > 50 * childNodes.length) {\n // quit for performance purposes\n splits.push(cssText);\n return splits;\n }\n const normPart = normalizeCssString(\n cssText.substring(0, k),\n _testNoPxNorm,\n );\n if (normPart.length === splitNorm) {\n splits.push(cssText.substring(0, k));\n cssText = cssText.substring(k);\n cssTextNorm = cssTextNorm.substring(splitNorm);\n break;\n } else if (normPart.length < splitNorm) {\n k += Math.max(\n 1,\n Math.floor((splitNorm - normPart.length) / normFactor),\n );\n } else {\n k -= Math.max(\n 1,\n Math.floor((normPart.length - splitNorm) * normFactor),\n );\n }\n }\n break;\n }\n }\n }\n }\n }\n splits.push(cssText); // either the full thing if no splits were found, or the last split\n return splits;\n}\n\nexport function markCssSplits(\n cssText: string,\n style: HTMLStyleElement,\n): string {\n return splitCssText(cssText, style).join('/* rr_split */');\n}\n","import type {\n MaskInputOptions,\n SlimDOMOptions,\n MaskTextFn,\n MaskInputFn,\n KeepIframeSrcFn,\n ICanvas,\n DialogAttributes,\n} from './types';\nimport { NodeType } from '@amplitude/rrweb-types';\nimport type {\n serializedNode,\n serializedNodeWithId,\n serializedElementNodeWithId,\n elementNode,\n attributes,\n mediaAttributes,\n DataURLOptions,\n} from '@amplitude/rrweb-types';\nimport {\n Mirror,\n is2DCanvasBlank,\n isElement,\n isShadowRoot,\n maskInputValue,\n isNativeShadowDom,\n stringifyStylesheet,\n getInputType,\n toLowerCase,\n extractFileExtension,\n absolutifyURLs,\n markCssSplits,\n} from './utils';\nimport dom from '@amplitude/rrweb-utils';\n\nconst _DEFAULT_BLOCKED_ELEMENT_BACKGROUND_COLOR = 'lightgrey';\n\nlet _id = 1;\nconst tagNameRegex = new RegExp('[^a-z0-9-_:]');\n\nexport const IGNORED_NODE = -2;\n\nexport function genId(): number {\n return _id++;\n}\n\nfunction getValidTagName(element: HTMLElement): Lowercase<string> {\n if (element instanceof HTMLFormElement) {\n return 'form';\n }\n\n const processedTagName = toLowerCase(element.tagName);\n\n if (tagNameRegex.test(processedTagName)) {\n // if the tag name is odd and we cannot extract\n // anything from the string, then we return a\n // generic div\n return 'div';\n }\n\n return processedTagName;\n}\n\nlet canvasService: HTMLCanvasElement | null;\nlet canvasCtx: CanvasRenderingContext2D | null;\n\n// eslint-disable-next-line no-control-regex\nconst SRCSET_NOT_SPACES = /^[^ \\t\\n\\r\\u000c]+/; // Don't use \\s, to avoid matching non-breaking space\n// eslint-disable-next-line no-control-regex\nconst SRCSET_COMMAS_OR_SPACES = /^[, \\t\\n\\r\\u000c]+/;\nfunction getAbsoluteSrcsetString(doc: Document, attributeValue: string) {\n /*\n run absoluteToDoc over every url in the srcset\n\n this is adapted from https://github.com/albell/parse-srcset/\n without the parsing of the descriptors (we return these as-is)\n parce-srcset is in turn based on\n https://html.spec.whatwg.org/multipage/embedded-content.html#parse-a-srcset-attribute\n */\n if (attributeValue.trim() === '') {\n return attributeValue;\n }\n\n let pos = 0;\n\n function collectCharacters(regEx: RegExp) {\n let chars: string;\n const match = regEx.exec(attributeValue.substring(pos));\n if (match) {\n chars = match[0];\n pos += chars.length;\n return chars;\n }\n return '';\n }\n\n const output = [];\n // eslint-disable-next-line no-constant-condition\n while (true) {\n collectCharacters(SRCSET_COMMAS_OR_SPACES);\n if (pos >= attributeValue.length) {\n break;\n }\n // don't split on commas within urls\n let url = collectCharacters(SRCSET_NOT_SPACES);\n if (url.slice(-1) === ',') {\n // aside: according to spec more than one comma at the end is a parse error, but we ignore that\n url = absoluteToDoc(doc, url.substring(0, url.length - 1));\n // the trailing comma splits the srcset, so the interpretion is that\n // another url will follow, and the descriptor is empty\n output.push(url);\n } else {\n let descriptorsStr = '';\n url = absoluteToDoc(doc, url);\n let inParens = false;\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const c = attributeValue.charAt(pos);\n if (c === '') {\n output.push((url + descriptorsStr).trim());\n break;\n } else if (!inParens) {\n if (c === ',') {\n pos += 1;\n output.push((url + descriptorsStr).trim());\n break; // parse the next url\n } else if (c === '(') {\n inParens = true;\n }\n } else {\n // in parenthesis; ignore commas\n // (parenthesis may be supported by future additions to spec)\n if (c === ')') {\n inParens = false;\n }\n }\n descriptorsStr += c;\n pos += 1;\n }\n }\n }\n return output.join(', ');\n}\n\nconst cachedDocument = new WeakMap<Document, HTMLAnchorElement>();\n\nexport function absoluteToDoc(doc: Document, attributeValue: string): string {\n if (!attributeValue || attributeValue.trim() === '') {\n return attributeValue;\n }\n\n return getHref(doc, attributeValue);\n}\n\nfunction isSVGElement(el: Element): boolean {\n return Boolean(el.tagName === 'svg' || (el as SVGElement).ownerSVGElement);\n}\n\nfunction getHref(doc: Document, customHref?: string) {\n let a = cachedDocument.get(doc);\n if (!a) {\n a = doc.createElement('a');\n cachedDocument.set(doc, a);\n }\n if (!customHref) {\n customHref = '';\n } else if (customHref.startsWith('blob:') || customHref.startsWith('data:')) {\n return customHref;\n }\n // note: using `new URL` is slower. See #1434 or https://jsbench.me/uqlud17rxo/1\n a.setAttribute('href', customHref);\n return a.href;\n}\n\nexport function transformAttribute(\n doc: Document,\n tagName: Lowercase<string>,\n name: Lowercase<string>,\n value: string | null,\n): string | null {\n if (!value) {\n return value;\n }\n\n // relative path in attribute\n if (\n name === 'src' ||\n (name === 'href' && !(tagName === 'use' && value[0] === '#'))\n ) {\n // href starts with a # is an id pointer for svg\n return absoluteToDoc(doc, value);\n } else if (name === 'xlink:href' && value[0] !== '#') {\n // xlink:href starts with # is an id pointer\n return absoluteToDoc(doc, value);\n } else if (\n name === 'background' &&\n (tagName === 'table' || tagName === 'td' || tagName === 'th')\n ) {\n return absoluteToDoc(doc, value);\n } else if (name === 'srcset') {\n return getAbsoluteSrcsetString(doc, value);\n } else if (name === 'style') {\n return absolutifyURLs(value, getHref(doc));\n } else if (tagName === 'object' && name === 'data') {\n return absoluteToDoc(doc, value);\n }\n\n return value;\n}\n\nexport function ignoreAttribute(\n tagName: string,\n name: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n _value: unknown,\n): boolean {\n return (tagName === 'video' || tagName === 'audio') && name === 'autoplay';\n}\n\nexport function _isBlockedElement(\n element: HTMLElement,\n blockClass: string | RegExp,\n blockSelector: string | null,\n): boolean {\n try {\n if (typeof blockClass === 'string') {\n if (element.classList.contains(blockClass)) {\n return true;\n }\n } else {\n for (let eIndex = element.classList.length; eIndex--; ) {\n const className = element.classList[eIndex];\n if (blockClass.test(className)) {\n return true;\n }\n }\n }\n if (blockSelector) {\n return element.matches(blockSelector);\n }\n } catch (e) {\n //\n }\n\n return false;\n}\n\nexport function classMatchesRegex(\n node: Node | null,\n regex: RegExp,\n checkAncestors: boolean,\n): boolean {\n if (!node) return false;\n if (node.nodeType !== node.ELEMENT_NODE) {\n if (!checkAncestors) return false;\n return classMatchesRegex(dom.parentNode(node), regex, checkAncestors);\n }\n\n for (let eIndex = (node as HTMLElement).classList.length; eIndex--; ) {\n const className = (node as HTMLElement).classList[eIndex];\n if (regex.test(className)) {\n return true;\n }\n }\n if (!checkAncestors) return false;\n return classMatchesRegex(dom.parentNode(node), regex, checkAncestors);\n}\n\nexport function needMaskingText(\n node: Node,\n maskTextClass: string | RegExp,\n maskTextSelector: string | null,\n checkAncestors: boolean,\n): boolean {\n let el: Element;\n if (isElement(node)) {\n el = node;\n if (!dom.childNodes(el).length) {\n // optimisation: we can avoid any of the below checks on leaf elements\n // as masking is applied to child text nodes only\n return false;\n }\n } else if (dom.parentElement(node) === null) {\n // should warn? maybe a text node isn't attached to a parent node yet?\n return false;\n } else {\n el = dom.parentElement(node)!;\n }\n try {\n if (typeof maskTextClass === 'string') {\n if (checkAncestors) {\n if (el.closest(`.${maskTextClass}`)) return true;\n } else {\n if (el.classList.contains(maskTextClass)) return true;\n }\n } else {\n if (classMatchesRegex(el, maskTextClass, checkAncestors)) return true;\n }\n if (maskTextSelector) {\n if (checkAncestors) {\n if (el.closest(maskTextSelector)) return true;\n } else {\n if (el.matches(maskTextSelector)) return true;\n }\n }\n } catch (e) {\n //\n }\n return false;\n}\n\n// https://stackoverflow.com/a/36155560\nfunction onceIframeLoaded(\n iframeEl: HTMLIFrameElement,\n listener: () => unknown,\n iframeLoadTimeout: number,\n) {\n const win = iframeEl.contentWindow;\n if (!win) {\n return;\n }\n // document is loading\n let fired = false;\n\n let readyState: DocumentReadyState;\n try {\n readyState = win.document.readyState;\n } catch (error) {\n return;\n }\n if (readyState !== 'complete') {\n const timer = setTimeout(() => {\n if (!fired) {\n listener();\n fired = true;\n }\n }, iframeLoadTimeout);\n iframeEl.addEventListener('load', () => {\n clearTimeout(timer);\n fired = true;\n listener();\n });\n return;\n }\n // check blank frame for Chrome\n const blankUrl = 'about:blank';\n if (\n win.location.href !== blankUrl ||\n iframeEl.src === blankUrl ||\n iframeEl.src === ''\n ) {\n // iframe was already loaded, make sure we wait to trigger the listener\n // till _after_ the mutation that found this iframe has had time to process\n setTimeout(listener, 0);\n\n return iframeEl.addEventListener('load', listener); // keep listing for future loads\n }\n // use default listener\n iframeEl.addEventListener('load', listener);\n}\n\nfunction onceStylesheetLoaded(\n link: HTMLLinkElement,\n listener: () => unknown,\n styleSheetLoadTimeout: number,\n) {\n let fired = false;\n let styleSheetLoaded: StyleSheet | null;\n try {\n styleSheetLoaded = link.sheet;\n } catch (error) {\n return;\n }\n\n if (styleSheetLoaded) return;\n\n const timer = setTimeout(() => {\n if (!fired) {\n listener();\n fired = true;\n }\n }, styleSheetLoadTimeout);\n\n link.addEventListener('load', () => {\n clearTimeout(timer);\n fired = true;\n listener();\n });\n}\n\nfunction serializeNode(\n n: Node,\n options: {\n doc: Document;\n mirror: Mirror;\n blockClass: string | RegExp;\n blockSelector: string | null;\n needsMask: boolean;\n inlineStylesheet: boolean;\n maskInputOptions: MaskInputOptions;\n maskTextFn: MaskTextFn | undefined;\n maskInputFn: MaskInputFn | undefined;\n dataURLOptions?: DataURLOptions;\n inlineImages: boolean;\n recordCanvas: boolean;\n keepIframeSrcFn: KeepIframeSrcFn;\n /**\n * `newlyAddedElement: true` skips scrollTop and scrollLeft check\n */\n newlyAddedElement?: boolean;\n cssCaptured?: boolean;\n applyBackgroundColorToBlockedElements?: boolean;\n },\n): serializedNode | false {\n const {\n doc,\n mirror,\n blockClass,\n blockSelector,\n needsMask,\n inlineStylesheet,\n maskInputOptions = {},\n maskTextFn,\n maskInputFn,\n dataURLOptions = {},\n inlineImages,\n recordCanvas,\n keepIframeSrcFn,\n newlyAddedElement = false,\n cssCaptured = false,\n applyBackgroundColorToBlockedElements = false,\n } = options;\n // Only record root id when document object is not the base document\n const rootId = getRootId(doc, mirror);\n switch (n.nodeType) {\n case n.DOCUMENT_NODE:\n if ((n as Document).compatMode !== 'CSS1Compat') {\n return {\n type: NodeType.Document,\n childNodes: [],\n compatMode: (n as Document).compatMode, // probably \"BackCompat\"\n };\n } else {\n return {\n type: NodeType.Document,\n childNodes: [],\n };\n }\n case n.DOCUMENT_TYPE_NODE:\n return {\n type: NodeType.DocumentType,\n name: (n as DocumentType).name,\n publicId: (n as DocumentType).publicId,\n systemId: (n as DocumentType).systemId,\n rootId,\n };\n case n.ELEMENT_NODE:\n return serializeElementNode(n as HTMLElement, {\n doc,\n blockClass,\n blockSelector,\n inlineStylesheet,\n maskInputOptions,\n maskInputFn,\n dataURLOptions,\n inlineImages,\n recordCanvas,\n keepIframeSrcFn,\n newlyAddedElement,\n rootId,\n applyBackgroundColorToBlockedElements,\n });\n case n.TEXT_NODE:\n return serializeTextNode(n as Text, {\n doc,\n needsMask,\n maskTextFn,\n rootId,\n cssCaptured,\n });\n case n.CDATA_SECTION_NODE:\n return {\n type: NodeType.CDATA,\n textContent: '',\n rootId,\n };\n case n.COMMENT_NODE:\n return {\n type: NodeType.Comment,\n textContent: dom.textContent(n as Comment) || '',\n rootId,\n };\n default:\n return false;\n }\n}\n\nfunction getRootId(doc: Document, mirror: Mirror): number | undefined {\n if (!mirror.hasNode(doc)) return undefined;\n const docId = mirror.getId(doc);\n return docId === 1 ? undefined : docId;\n}\n\nfunction serializeTextNode(\n n: Text,\n options: {\n doc: Document;\n needsMask: boolean;\n maskTextFn: MaskTextFn | undefined;\n rootId: number | undefined;\n cssCaptured?: boolean;\n },\n): serializedNode {\n const { needsMask, maskTextFn, rootId, cssCaptured } = options;\n // The parent node may not be a html element which has a tagName attribute.\n // So just let it be undefined which is ok in this use case.\n const parent = dom.parentNode(n);\n const parentTagName = parent && (parent as HTMLElement).tagName;\n let textContent: string | null = '';\n const isStyle = parentTagName === 'STYLE' ? true : undefined;\n const isScript = parentTagName === 'SCRIPT' ? true : undefined;\n if (isScript) {\n textContent = 'SCRIPT_PLACEHOLDER';\n } else if (!cssCaptured) {\n textContent = dom.textContent(n);\n if (isStyle && textContent) {\n // mutation only: we don't need to use stringifyStylesheet\n // as a <style> text node mutation obliterates any previous\n // programmatic rule manipulation (.insertRule etc.)\n // so the current textContent represents the most up to date state\n textContent = absolutifyURLs(textContent, getHref(options.doc));\n }\n }\n if (!isStyle && !isScript && textContent && needsMask) {\n textContent = maskTextFn\n ? maskTextFn(textContent, dom.parentElement(n))\n : textContent.replace(/[\\S]/g, '*');\n }\n\n return {\n type: NodeType.Text,\n textContent: textContent || '',\n rootId,\n };\n}\n\nfunction serializeElementNode(\n n: HTMLElement,\n options: {\n doc: Document;\n blockClass: string | RegExp;\n blockSelector: string | null;\n inlineStylesheet: boolean;\n maskInputOptions: MaskInputOptions;\n maskInputFn: MaskInputFn | undefined;\n dataURLOptions?: DataURLOptions;\n inlineImages: boolean;\n recordCanvas: boolean;\n keepIframeSrcFn: KeepIframeSrcFn;\n /**\n * `newlyAddedElement: true` skips scrollTop and scrollLeft check\n */\n newlyAddedElement?: boolean;\n rootId: number | undefined;\n applyBackgroundColorToBlockedElements?: boolean;\n },\n): serializedNode | false {\n const {\n doc,\n blockClass,\n blockSelector,\n inlineStylesheet,\n maskInputOptions = {},\n maskInputFn,\n dataURLOptions = {},\n inlineImages,\n recordCanvas,\n keepIframeSrcFn,\n newlyAddedElement = false,\n rootId,\n applyBackgroundColorToBlockedElements = false,\n } = options;\n const needBlock = _isBlockedElement(n, blockClass, blockSelector);\n const tagName = getValidTagName(n);\n let attributes: attributes = {};\n const len = n.attributes.length;\n for (let i = 0; i < len; i++) {\n const attr = n.attributes[i];\n if (!ignoreAttribute(tagName, attr.name, attr.value)) {\n attributes[attr.name] = transformAttribute(\n doc,\n tagName,\n toLowerCase(attr.name),\n attr.value,\n );\n }\n }\n // remote css\n if (tagName === 'link' && inlineStylesheet) {\n //TODO: maybe replace this `.styleSheets` with original one\n const stylesheet = Array.from(doc.styleSheets).find((s) => {\n return s.h