/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { NodeType as RRNodeType } from 'rrweb-snapshot'; import type { NWSAPI } from 'nwsapi'; import type { CSSStyleDeclaration as CSSStyleDeclarationType } from 'cssstyle'; import { BaseRRCDATASectionImpl, BaseRRCommentImpl, BaseRRDocumentImpl, BaseRRDocumentTypeImpl, BaseRRElementImpl, BaseRRMediaElementImpl, BaseRRNode, BaseRRTextImpl, ClassList, IRRDocument, CSSStyleDeclaration, } from 'rrdom'; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires const nwsapi = require('nwsapi'); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires const cssom = require('cssom'); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-var-requires const cssstyle = require('cssstyle'); export class RRNode extends BaseRRNode {} export class RRWindow { scrollLeft = 0; scrollTop = 0; scrollTo(options?: ScrollToOptions) { if (!options) return; if (typeof options.left === 'number') this.scrollLeft = options.left; if (typeof options.top === 'number') this.scrollTop = options.top; } } export class RRDocument extends BaseRRDocumentImpl(RRNode) implements IRRDocument { readonly nodeName: '#document' = '#document'; private _nwsapi: NWSAPI; get nwsapi() { if (!this._nwsapi) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call this._nwsapi = nwsapi({ document: this as unknown as Document, DOMException: null as unknown as new ( message?: string, name?: string, ) => DOMException, }); this._nwsapi.configure({ LOGERRORS: false, IDS_DUPES: true, MIXEDCASE: true, }); } return this._nwsapi; } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore get documentElement(): RRElement | null { return super.documentElement as RRElement | null; } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore get body(): RRElement | null { return super.body as RRElement | null; } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore get head() { return super.head as RRElement | null; } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore get implementation(): RRDocument { return this; } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore get firstElementChild(): RRElement | null { return this.documentElement; } appendChild(childNode: RRNode) { return super.appendChild(childNode); } insertBefore(newChild: RRNode, refChild: RRNode | null) { return super.insertBefore(newChild, refChild); } querySelectorAll(selectors: string): RRNode[] { return this.nwsapi.select(selectors) as unknown as RRNode[]; } getElementsByTagName(tagName: string): RRElement[] { if (this.documentElement) return this.documentElement.getElementsByTagName(tagName); return []; } getElementsByClassName(className: string): RRElement[] { if (this.documentElement) return this.documentElement.getElementsByClassName(className); return []; } getElementById(elementId: string): RRElement | null { if (this.documentElement) return this.documentElement.getElementById(elementId); return null; } createDocument( // eslint-disable-next-line @typescript-eslint/no-unused-vars _namespace: string | null, // eslint-disable-next-line @typescript-eslint/no-unused-vars _qualifiedName: string | null, // eslint-disable-next-line @typescript-eslint/no-unused-vars _doctype?: DocumentType | null, ) { return new RRDocument(); } createDocumentType( qualifiedName: string, publicId: string, systemId: string, ) { const documentTypeNode = new RRDocumentType( qualifiedName, publicId, systemId, ); documentTypeNode.ownerDocument = this; return documentTypeNode; } createElement( tagName: K, ): RRElementType; createElement(tagName: string): RRElement; createElement(tagName: string) { const upperTagName = tagName.toUpperCase(); let element; switch (upperTagName) { case 'AUDIO': case 'VIDEO': element = new RRMediaElement(upperTagName); break; case 'IFRAME': element = new RRIFrameElement(upperTagName); break; case 'IMG': element = new RRImageElement(upperTagName); break; case 'CANVAS': element = new RRCanvasElement(upperTagName); break; case 'STYLE': element = new RRStyleElement(upperTagName); break; default: element = new RRElement(upperTagName); break; } element.ownerDocument = this; return element; } createElementNS(_namespaceURI: string, qualifiedName: string) { return this.createElement(qualifiedName as keyof HTMLElementTagNameMap); } createComment(data: string) { const commentNode = new RRComment(data); commentNode.ownerDocument = this; return commentNode; } createCDATASection(data: string) { const sectionNode = new RRCDATASection(data); sectionNode.ownerDocument = this; return sectionNode; } createTextNode(data: string) { const textNode = new RRText(data); textNode.ownerDocument = this; return textNode; } } export class RRDocumentType extends BaseRRDocumentTypeImpl(RRNode) {} export class RRElement extends BaseRRElementImpl(RRNode) { private _style: CSSStyleDeclarationType; constructor(tagName: string) { super(tagName); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access this._style = new cssstyle.CSSStyleDeclaration(); const style = this._style; Object.defineProperty(this.attributes, 'style', { get() { return style.cssText; }, set(cssText: string) { style.cssText = cssText; }, }); } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore get style() { return this._style as unknown as CSSStyleDeclaration; } attachShadow(_init: ShadowRootInit): RRElement { return super.attachShadow(_init) as RRElement; } appendChild(newChild: RRNode): RRNode { return super.appendChild(newChild) as RRNode; } insertBefore(newChild: RRNode, refChild: RRNode | null): RRNode { return super.insertBefore(newChild, refChild) as RRNode; } getAttribute(name: string) { const upperName = name && name.toLowerCase(); if (upperName in this.attributes) return this.attributes[upperName]; return null; } setAttribute(name: string, attribute: string) { this.attributes[name.toLowerCase()] = attribute; } removeAttribute(name: string) { delete this.attributes[name.toLowerCase()]; } get firstElementChild(): RRElement | null { for (const child of this.childNodes) if (child.RRNodeType === RRNodeType.Element) return child as RRElement; return null; } get nextElementSibling(): RRElement | null { const parentNode = this.parentNode; if (!parentNode) return null; const siblings = parentNode.childNodes; const index = siblings.indexOf(this); for (let i = index + 1; i < siblings.length; i++) if (siblings[i] instanceof RRElement) return siblings[i] as RRElement; return null; } querySelectorAll(selectors: string): RRNode[] { const result: RRElement[] = []; if (this.ownerDocument !== null) { (this.ownerDocument as RRDocument).nwsapi.select( selectors, this as unknown as Element, (element) => { if ((element as unknown as RRElement) !== this) result.push(element as unknown as RRElement); }, ) as unknown as RRNode[]; } return result; } getElementById(elementId: string): RRElement | null { if (this.id === elementId) return this; for (const child of this.childNodes) { if (child instanceof RRElement) { const result = child.getElementById(elementId); if (result !== null) return result; } } return null; } getElementsByClassName(className: string): RRElement[] { let elements: RRElement[] = []; const queryClassList = new ClassList(className); // Make sure this element has all queried class names. if ( this instanceof RRElement && queryClassList.classes.filter((queriedClassName) => this.classList.classes.some((name) => name === queriedClassName), ).length == queryClassList.classes.length ) elements.push(this); for (const child of this.childNodes) { if (child instanceof RRElement) elements = elements.concat(child.getElementsByClassName(className)); } return elements; } getElementsByTagName(tagName: string): RRElement[] { let elements: RRElement[] = []; const normalizedTagName = tagName.toUpperCase(); if (this instanceof RRElement && this.tagName === normalizedTagName) elements.push(this); for (const child of this.childNodes) { if (child instanceof RRElement) elements = elements.concat(child.getElementsByTagName(tagName)); } return elements; } } export class RRImageElement extends RRElement { src: string; width: number; height: number; onload: ((this: GlobalEventHandlers, ev: Event) => unknown) | null; } export class RRMediaElement extends BaseRRMediaElementImpl(RRElement) {} export class RRCanvasElement extends RRElement { /** * This is just a dummy implementation to prevent rrweb replayer from drawing mouse tail. If further analysis of canvas is needed, we may implement it with node-canvas. */ getContext(): CanvasRenderingContext2D | null { return null; } } export class RRStyleElement extends RRElement { private _sheet: CSSStyleSheet | null = null; get sheet() { if (!this._sheet) { let result = ''; for (const child of this.childNodes) if (child.RRNodeType === RRNodeType.Text) result += (child as RRText).textContent; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call this._sheet = cssom.parse(result); } return this._sheet; } } export class RRIFrameElement extends RRElement { width = ''; height = ''; src = ''; contentDocument: RRDocument = new RRDocument(); contentWindow: RRWindow = new RRWindow(); constructor(tagName: string) { super(tagName); const htmlElement = this.contentDocument.createElement('HTML'); this.contentDocument.appendChild(htmlElement); htmlElement.appendChild(this.contentDocument.createElement('HEAD')); htmlElement.appendChild(this.contentDocument.createElement('BODY')); } } export class RRText extends BaseRRTextImpl(RRNode) { readonly nodeName: '#text' = '#text'; } export class RRComment extends BaseRRCommentImpl(RRNode) { readonly nodeName: '#comment' = '#comment'; } export class RRCDATASection extends BaseRRCDATASectionImpl(RRNode) { readonly nodeName: '#cdata-section' = '#cdata-section'; } interface RRElementTagNameMap { audio: RRMediaElement; canvas: RRCanvasElement; iframe: RRIFrameElement; img: RRImageElement; style: RRStyleElement; video: RRMediaElement; } type RRElementType = K extends keyof RRElementTagNameMap ? RRElementTagNameMap[K] : RRElement;