refactoring public API

This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent 6852da5fe5
commit a9ef2cfa83
5 changed files with 146 additions and 70 deletions

View File

@@ -77,9 +77,12 @@ export function addHoverClass(cssText: string): string {
function buildNode(
n: serializedNodeWithId,
doc: Document,
HACK_CSS: boolean,
options: {
doc: Document;
hackCss: boolean;
},
): Node | null {
const { doc, hackCss } = options;
switch (n.type) {
case NodeType.Document:
return doc.implementation.createDocument(null, '', null);
@@ -109,7 +112,7 @@ function buildNode(
const isTextarea = tagName === 'textarea' && name === 'value';
const isRemoteOrDynamicCss =
tagName === 'style' && name === '_cssText';
if (isRemoteOrDynamicCss && HACK_CSS) {
if (isRemoteOrDynamicCss && hackCss) {
value = addHoverClass(value);
}
if (isTextarea || isRemoteOrDynamicCss) {
@@ -177,7 +180,7 @@ function buildNode(
return node;
case NodeType.Text:
return doc.createTextNode(
n.isStyle && HACK_CSS ? addHoverClass(n.textContent) : n.textContent,
n.isStyle && hackCss ? addHoverClass(n.textContent) : n.textContent,
);
case NodeType.CDATA:
return doc.createCDATASection(n.textContent);
@@ -190,12 +193,15 @@ function buildNode(
export function buildNodeWithSN(
n: serializedNodeWithId,
doc: Document,
map: idNodeMap,
skipChild = false,
HACK_CSS = true,
options: {
doc: Document;
map: idNodeMap;
skipChild?: boolean;
hackCss: boolean;
},
): INode | null {
let node = buildNode(n, doc, HACK_CSS);
const { doc, map, skipChild = false, hackCss = true } = options;
let node = buildNode(n, { doc, hackCss });
if (!node) {
return null;
}
@@ -214,7 +220,12 @@ export function buildNodeWithSN(
!skipChild
) {
for (const childN of n.childNodes) {
const childNode = buildNodeWithSN(childN, doc, map, false, HACK_CSS);
const childNode = buildNodeWithSN(childN, {
doc,
map,
skipChild: false,
hackCss,
});
if (!childNode) {
console.warn('Failed to rebuild', childN);
} else {
@@ -259,15 +270,20 @@ function handleScroll(node: INode) {
function rebuild(
n: serializedNodeWithId,
doc: Document,
onVisit?: (node: INode) => unknown,
/**
* This is not a public API yet, just for POC
*/
HACK_CSS: boolean = true,
options: {
doc: Document;
onVisit?: (node: INode) => unknown;
hackCss?: boolean;
},
): [Node | null, idNodeMap] {
const { doc, onVisit, hackCss = true } = options;
const idNodeMap: idNodeMap = {};
const node = buildNodeWithSN(n, doc, idNodeMap, false, HACK_CSS);
const node = buildNodeWithSN(n, {
doc,
map: idNodeMap,
skipChild: false,
hackCss,
});
visit(idNodeMap, (visitedNode) => {
if (onVisit) {
onVisit(visitedNode);

View File

@@ -187,13 +187,23 @@ export function _isBlockedElement(
function serializeNode(
n: Node,
doc: Document,
blockClass: string | RegExp,
blockSelector: string | null,
inlineStylesheet: boolean,
maskInputOptions: MaskInputOptions = {},
recordCanvas: boolean,
options: {
doc: Document;
blockClass: string | RegExp;
blockSelector: string | null;
inlineStylesheet: boolean;
maskInputOptions: MaskInputOptions;
recordCanvas: boolean;
},
): serializedNode | false {
const {
doc,
blockClass,
blockSelector,
inlineStylesheet,
maskInputOptions = {},
recordCanvas,
} = options;
switch (n.nodeType) {
case n.DOCUMENT_NODE:
return {
@@ -437,26 +447,39 @@ function slimDOMExcluded(
export function serializeNodeWithId(
n: Node | INode,
doc: Document,
map: idNodeMap,
blockClass: string | RegExp,
blockSelector: string | null,
skipChild = false,
inlineStylesheet = true,
maskInputOptions?: MaskInputOptions,
slimDOMOptions: SlimDOMOptions = {},
recordCanvas?: boolean,
preserveWhiteSpace = true,
options: {
doc: Document;
map: idNodeMap;
blockClass: string | RegExp;
blockSelector: string | null;
skipChild: boolean;
inlineStylesheet: boolean;
maskInputOptions?: MaskInputOptions;
slimDOMOptions: SlimDOMOptions;
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
},
): serializedNodeWithId | null {
const _serializedNode = serializeNode(
n,
const {
doc,
map,
blockClass,
blockSelector,
skipChild = false,
inlineStylesheet = true,
maskInputOptions = {},
slimDOMOptions,
recordCanvas = false,
} = options;
let { preserveWhiteSpace = true } = options;
const _serializedNode = serializeNode(n, {
doc,
blockClass,
blockSelector,
inlineStylesheet,
maskInputOptions,
recordCanvas || false,
);
recordCanvas,
});
if (!_serializedNode) {
// TODO: dev only
console.warn(n, 'not serialized');
@@ -504,8 +527,7 @@ export function serializeNodeWithId(
preserveWhiteSpace = false;
}
for (const childN of Array.from(n.childNodes)) {
const serializedChildNode = serializeNodeWithId(
childN,
const serializedChildNode = serializeNodeWithId(childN, {
doc,
map,
blockClass,
@@ -516,7 +538,7 @@ export function serializeNodeWithId(
slimDOMOptions,
recordCanvas,
preserveWhiteSpace,
);
});
if (serializedChildNode) {
serializedNode.childNodes.push(serializedChildNode);
}
@@ -527,16 +549,26 @@ export function serializeNodeWithId(
function snapshot(
n: Document,
blockClass: string | RegExp = 'rr-block',
inlineStylesheet = true,
maskAllInputsOrOptions: boolean | MaskInputOptions,
slimDOMSensibleOrOptions: boolean | SlimDOMOptions,
recordCanvas?: boolean,
blockSelector: string | null = null,
options?: {
blockClass?: string | RegExp;
inlineStylesheet?: boolean;
maskAllInputs?: boolean | MaskInputOptions;
slimDOM?: boolean | SlimDOMOptions;
recordCanvas?: boolean;
blockSelector?: string | null;
},
): [serializedNodeWithId | null, idNodeMap] {
const {
blockClass = 'rr-block',
inlineStylesheet = true,
recordCanvas = false,
blockSelector = null,
maskAllInputs = false,
slimDOM = false,
} = options || {};
const idNodeMap: idNodeMap = {};
const maskInputOptions: MaskInputOptions =
maskAllInputsOrOptions === true
maskAllInputs === true
? {
color: true,
date: true,
@@ -554,40 +586,39 @@ function snapshot(
textarea: true,
select: true,
}
: maskAllInputsOrOptions === false
: maskAllInputs === false
? {}
: maskAllInputsOrOptions;
: maskAllInputs;
const slimDOMOptions: SlimDOMOptions =
slimDOMSensibleOrOptions === true || slimDOMSensibleOrOptions === 'all'
slimDOM === true || slimDOM === 'all'
? // if true: set of sensible options that should not throw away any information
{
script: true,
comment: true,
headFavicon: true,
headWhitespace: true,
headMetaDescKeywords: slimDOMSensibleOrOptions === 'all', // destructive
headMetaDescKeywords: slimDOM === 'all', // destructive
headMetaSocial: true,
headMetaRobots: true,
headMetaHttpEquiv: true,
headMetaAuthorship: true,
headMetaVerification: true,
}
: slimDOMSensibleOrOptions === false
: slimDOM === false
? {}
: slimDOMSensibleOrOptions;
: slimDOM;
return [
serializeNodeWithId(
n,
n,
idNodeMap,
serializeNodeWithId(n, {
doc: n,
map: idNodeMap,
blockClass,
blockSelector,
false,
skipChild: false,
inlineStylesheet,
maskInputOptions,
slimDOMOptions,
recordCanvas,
),
}),
idNodeMap,
];
}

View File

@@ -11,7 +11,7 @@ import { SnapshotState, toMatchSnapshot } from 'jest-snapshot';
import { Suite } from 'mocha';
const htmlFolder = path.join(__dirname, 'html');
const htmls = fs.readdirSync(htmlFolder).map(filePath => {
const htmls = fs.readdirSync(htmlFolder).map((filePath) => {
const raw = fs.readFileSync(path.resolve(htmlFolder, filePath), 'utf-8');
return {
filePath,
@@ -24,7 +24,7 @@ interface IMimeType {
}
const server = () =>
new Promise<http.Server>(resolve => {
new Promise<http.Server>((resolve) => {
const mimeType: IMimeType = {
'.html': 'text/html',
'.js': 'text/javascript',
@@ -73,7 +73,7 @@ interface ISuite extends Suite {
code: string;
}
describe('integration tests', function(this: ISuite) {
describe('integration tests', function (this: ISuite) {
before(async () => {
this.server = await server();
this.browser = await puppeteer.launch({
@@ -102,16 +102,18 @@ describe('integration tests', function(this: ISuite) {
const page: puppeteer.Page = await this.browser.newPage();
// console for debug
// tslint:disable-next-line: no-console
page.on('console', msg => console.log(msg.text()));
page.on('console', (msg) => console.log(msg.text()));
await page.goto(`http://localhost:3030/html`);
await page.setContent(html.src, {
waitUntil: 'load',
});
const rebuildHtml = (await page.evaluate(`${this.code}
const rebuildHtml = (
await page.evaluate(`${this.code}
const x = new XMLSerializer();
const [snap] = rrweb.snapshot(document);
x.serializeToString(rrweb.rebuild(snap, document)[0]);
`)).replace(/\n\n/g, '');
x.serializeToString(rrweb.rebuild(snap, { doc: document })[0]);
`)
).replace(/\n\n/g, '');
const result = matchSnapshot(rebuildHtml, __filename, title);
assert(result.pass, result.pass ? '' : result.report());
}).timeout(5000);

13
typings/rebuild.d.ts vendored
View File

@@ -1,5 +1,14 @@
import { serializedNodeWithId, idNodeMap, INode } from './types';
export declare function addHoverClass(cssText: string): string;
export declare function buildNodeWithSN(n: serializedNodeWithId, doc: Document, map: idNodeMap, skipChild?: boolean, HACK_CSS?: boolean): INode | null;
declare function rebuild(n: serializedNodeWithId, doc: Document, onVisit?: (node: INode) => unknown, HACK_CSS?: boolean): [Node | null, idNodeMap];
export declare function buildNodeWithSN(n: serializedNodeWithId, options: {
doc: Document;
map: idNodeMap;
skipChild?: boolean;
hackCss: boolean;
}): INode | null;
declare function rebuild(n: serializedNodeWithId, options: {
doc: Document;
onVisit?: (node: INode) => unknown;
hackCss?: boolean;
}): [Node | null, idNodeMap];
export default rebuild;

22
typings/snapshot.d.ts vendored
View File

@@ -4,8 +4,26 @@ export declare function absoluteToStylesheet(cssText: string | null, href: strin
export declare function absoluteToDoc(doc: Document, attributeValue: string): string;
export declare function transformAttribute(doc: Document, name: string, value: string): string;
export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null): boolean;
export declare function serializeNodeWithId(n: Node | INode, doc: Document, map: idNodeMap, blockClass: string | RegExp, blockSelector: string | null, skipChild?: boolean, inlineStylesheet?: boolean, maskInputOptions?: MaskInputOptions, slimDOMOptions?: SlimDOMOptions, recordCanvas?: boolean, preserveWhiteSpace?: boolean): serializedNodeWithId | null;
declare function snapshot(n: Document, blockClass: string | RegExp | undefined, inlineStylesheet: boolean | undefined, maskAllInputsOrOptions: boolean | MaskInputOptions, slimDOMSensibleOrOptions: boolean | SlimDOMOptions, recordCanvas?: boolean, blockSelector?: string | null): [serializedNodeWithId | null, idNodeMap];
export declare function serializeNodeWithId(n: Node | INode, options: {
doc: Document;
map: idNodeMap;
blockClass: string | RegExp;
blockSelector: string | null;
skipChild: boolean;
inlineStylesheet: boolean;
maskInputOptions?: MaskInputOptions;
slimDOMOptions: SlimDOMOptions;
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
}): serializedNodeWithId | null;
declare function snapshot(n: Document, options?: {
blockClass?: string | RegExp;
inlineStylesheet?: boolean;
maskAllInputs?: boolean | MaskInputOptions;
slimDOM?: boolean | SlimDOMOptions;
recordCanvas?: boolean;
blockSelector?: string | null;
}): [serializedNodeWithId | null, idNodeMap];
export declare function visitSnapshot(node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown): void;
export declare function cleanupSnapshot(): void;
export default snapshot;