Enable to mask texts (#73)

* chore: reorder options

* feat: enable to mask texts

* feat: add the default mask function

* refactor: rename options to identify the difference between  mask text and mask input

* test: add tests about masking
This commit is contained in:
re-fort
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 74706eeac6
commit 89cdf67234
8 changed files with 127 additions and 7 deletions

View File

@@ -3,6 +3,7 @@ import snapshot, {
transformAttribute,
visitSnapshot,
cleanupSnapshot,
needMaskingText,
IGNORED_NODE,
} from './snapshot';
import rebuild, { buildNodeWithSN, addHoverClass } from './rebuild';
@@ -18,5 +19,6 @@ export {
transformAttribute,
visitSnapshot,
cleanupSnapshot,
needMaskingText,
IGNORED_NODE,
};

View File

@@ -7,6 +7,7 @@ import {
idNodeMap,
MaskInputOptions,
SlimDOMOptions,
MaskTextFn,
} from './types';
import { isElement, isShadowRoot } from './utils';
@@ -206,6 +207,40 @@ export function _isBlockedElement(
return false;
}
export function needMaskingText(
node: Node | null,
maskTextClass: string | RegExp,
maskTextSelector: string | null,
): boolean {
if (!node) {
return false;
}
if (node.nodeType === node.ELEMENT_NODE) {
if (typeof maskTextClass === 'string') {
if ((node as HTMLElement).classList.contains(maskTextClass)) {
return true;
}
} else {
(node as HTMLElement).classList.forEach((className) => {
if (maskTextClass.test(className)) {
return true;
}
});
}
if (maskTextSelector) {
if ((node as HTMLElement).matches(maskTextSelector)) {
return true;
}
}
return needMaskingText(node.parentNode, maskTextClass, maskTextSelector);
}
if (node.nodeType === node.TEXT_NODE) {
// check parent node since text node do not have class name
return needMaskingText(node.parentNode, maskTextClass, maskTextSelector);
}
return needMaskingText(node.parentNode, maskTextClass, maskTextSelector);
}
// https://stackoverflow.com/a/36155560
function onceIframeLoaded(
iframeEl: HTMLIFrameElement,
@@ -259,8 +294,11 @@ function serializeNode(
doc: Document;
blockClass: string | RegExp;
blockSelector: string | null;
maskTextClass: string | RegExp;
maskTextSelector: string | null;
inlineStylesheet: boolean;
maskInputOptions: MaskInputOptions;
maskTextFn: MaskTextFn | undefined;
recordCanvas: boolean;
},
): serializedNode | false {
@@ -268,8 +306,11 @@ function serializeNode(
doc,
blockClass,
blockSelector,
maskTextClass,
maskTextSelector,
inlineStylesheet,
maskInputOptions = {},
maskTextFn,
recordCanvas,
} = options;
// Only record root id when document object is not the base document
@@ -412,12 +453,23 @@ function serializeNode(
n.parentNode && (n.parentNode as HTMLElement).tagName;
let textContent = (n as Text).textContent;
const isStyle = parentTagName === 'STYLE' ? true : undefined;
const isScript = parentTagName === 'SCRIPT' ? true : undefined;
if (isStyle && textContent) {
textContent = absoluteToStylesheet(textContent, getHref());
}
if (parentTagName === 'SCRIPT') {
if (isScript) {
textContent = 'SCRIPT_PLACEHOLDER';
}
if (
!isStyle &&
!isScript &&
needMaskingText(n, maskTextClass, maskTextSelector) &&
textContent
) {
textContent = maskTextFn
? maskTextFn(textContent)
: textContent.replace(/[\S]/g, '*');
}
return {
type: NodeType.Text,
textContent: textContent || '',
@@ -540,9 +592,12 @@ export function serializeNodeWithId(
map: idNodeMap;
blockClass: string | RegExp;
blockSelector: string | null;
maskTextClass: string | RegExp;
maskTextSelector: string | null;
skipChild: boolean;
inlineStylesheet: boolean;
maskInputOptions?: MaskInputOptions;
maskTextFn: MaskTextFn | undefined;
slimDOMOptions: SlimDOMOptions;
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
@@ -556,9 +611,12 @@ export function serializeNodeWithId(
map,
blockClass,
blockSelector,
maskTextClass,
maskTextSelector,
skipChild = false,
inlineStylesheet = true,
maskInputOptions = {},
maskTextFn,
slimDOMOptions,
recordCanvas = false,
onSerialize,
@@ -570,8 +628,11 @@ export function serializeNodeWithId(
doc,
blockClass,
blockSelector,
maskTextClass,
maskTextSelector,
inlineStylesheet,
maskInputOptions,
maskTextFn,
recordCanvas,
});
if (!_serializedNode) {
@@ -628,9 +689,12 @@ export function serializeNodeWithId(
map,
blockClass,
blockSelector,
maskTextClass,
maskTextSelector,
skipChild,
inlineStylesheet,
maskInputOptions,
maskTextFn,
slimDOMOptions,
recordCanvas,
preserveWhiteSpace,
@@ -675,9 +739,12 @@ export function serializeNodeWithId(
map,
blockClass,
blockSelector,
maskTextClass,
maskTextSelector,
skipChild: false,
inlineStylesheet,
maskInputOptions,
maskTextFn,
slimDOMOptions,
recordCanvas,
preserveWhiteSpace,
@@ -702,11 +769,14 @@ function snapshot(
n: Document,
options?: {
blockClass?: string | RegExp;
blockSelector?: string | null;
maskTextClass?: string | RegExp;
maskTextSelector?: string | null;
inlineStylesheet?: boolean;
maskAllInputs?: boolean | MaskInputOptions;
maskTextFn?: MaskTextFn;
slimDOM?: boolean | SlimDOMOptions;
recordCanvas?: boolean;
blockSelector?: string | null;
preserveWhiteSpace?: boolean;
onSerialize?: (n: INode) => unknown;
onIframeLoad?: (iframeINode: INode, node: serializedNodeWithId) => unknown;
@@ -715,10 +785,13 @@ function snapshot(
): [serializedNodeWithId | null, idNodeMap] {
const {
blockClass = 'rr-block',
blockSelector = null,
maskTextClass = 'rr-mask',
maskTextSelector = null,
inlineStylesheet = true,
recordCanvas = false,
blockSelector = null,
maskAllInputs = false,
maskTextFn,
slimDOM = false,
preserveWhiteSpace,
onSerialize,
@@ -772,9 +845,12 @@ function snapshot(
map: idNodeMap,
blockClass,
blockSelector,
maskTextClass,
maskTextSelector,
skipChild: false,
inlineStylesheet,
maskInputOptions,
maskTextFn,
slimDOMOptions,
recordCanvas,
preserveWhiteSpace,

View File

@@ -105,3 +105,5 @@ export type SlimDOMOptions = Partial<{
headMetaAuthorship: boolean;
headMetaVerification: boolean;
}>;
export type MaskTextFn = (text: string) => string;

View File

@@ -202,6 +202,21 @@ exports[`[html file]: invalid-tagname.html 1`] = `
</body></html>"
`;
exports[`[html file]: mask-text.html 1`] = `
"<!DOCTYPE html><html xmlns=\\"http://www.w3.org/1999/xhtml\\" lang=\\"en\\"><head>
<meta charset=\\"UTF-8\\" />
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" />
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\" />
<title>Document</title>
</head> <body>
<p class=\\"rr-mask\\">**** *</p>
<div class=\\"rr-mask\\">
<span>**** *</span>
</div>
<div class=\\"rr-mask\\">**** *</div>
</body></html>"
`;
exports[`[html file]: picture.html 1`] = `
"<html xmlns=\\"http://www.w3.org/1999/xhtml\\"><head></head><body>
<picture>

17
test/html/mask-text.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<p class="rr-mask">mask 1</p>
<div class="rr-mask">
<span>mask 2</span>
</div>
<div class="rr-mask">mask 3</div>
</body>
</html>

4
typings/index.d.ts vendored
View File

@@ -1,5 +1,5 @@
import snapshot, { serializeNodeWithId, transformAttribute, visitSnapshot, cleanupSnapshot, IGNORED_NODE } from './snapshot';
import snapshot, { serializeNodeWithId, transformAttribute, visitSnapshot, cleanupSnapshot, needMaskingText, IGNORED_NODE } from './snapshot';
import rebuild, { buildNodeWithSN, addHoverClass } from './rebuild';
export * from './types';
export * from './utils';
export { snapshot, serializeNodeWithId, rebuild, buildNodeWithSN, addHoverClass, transformAttribute, visitSnapshot, cleanupSnapshot, IGNORED_NODE, };
export { snapshot, serializeNodeWithId, rebuild, buildNodeWithSN, addHoverClass, transformAttribute, visitSnapshot, cleanupSnapshot, needMaskingText, IGNORED_NODE, };

11
typings/snapshot.d.ts vendored
View File

@@ -1,17 +1,21 @@
import { serializedNodeWithId, INode, idNodeMap, MaskInputOptions, SlimDOMOptions } from './types';
import { serializedNodeWithId, INode, idNodeMap, MaskInputOptions, MaskTextFn, SlimDOMOptions } from './types';
export declare const IGNORED_NODE = -2;
export declare function absoluteToStylesheet(cssText: string | null, href: string): string;
export declare function absoluteToDoc(doc: Document, attributeValue: string): string;
export declare function transformAttribute(doc: Document, tagName: string, name: string, value: string): string;
export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null): boolean;
export declare function needMaskingText(node: Node | null, maskTextClass: string | RegExp, maskTextSelector: string | null): boolean;
export declare function serializeNodeWithId(n: Node | INode, options: {
doc: Document;
map: idNodeMap;
blockClass: string | RegExp;
blockSelector: string | null;
maskTextClass: string | RegExp;
maskTextSelector: string | null;
skipChild: boolean;
inlineStylesheet: boolean;
maskInputOptions?: MaskInputOptions;
maskTextFn?: MaskTextFn;
slimDOMOptions: SlimDOMOptions;
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
@@ -21,11 +25,14 @@ export declare function serializeNodeWithId(n: Node | INode, options: {
}): serializedNodeWithId | null;
declare function snapshot(n: Document, options?: {
blockClass?: string | RegExp;
blockSelector?: string | null;
maskTextClass?: string | RegExp;
maskTextSelector?: string | null;
inlineStylesheet?: boolean;
maskAllInputs?: boolean | MaskInputOptions;
maskTextFn?: MaskTextFn;
slimDOM?: boolean | SlimDOMOptions;
recordCanvas?: boolean;
blockSelector?: string | null;
preserveWhiteSpace?: boolean;
onSerialize?: (n: INode) => unknown;
onIframeLoad?: (iframeINode: INode, node: serializedNodeWithId) => unknown;

1
typings/types.d.ts vendored
View File

@@ -86,3 +86,4 @@ export declare type SlimDOMOptions = Partial<{
headMetaAuthorship: boolean;
headMetaVerification: boolean;
}>;
export declare type MaskTextFn = (text: string) => string;