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:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -105,3 +105,5 @@ export type SlimDOMOptions = Partial<{
|
||||
headMetaAuthorship: boolean;
|
||||
headMetaVerification: boolean;
|
||||
}>;
|
||||
|
||||
export type MaskTextFn = (text: string) => string;
|
||||
|
||||
@@ -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
17
test/html/mask-text.html
Normal 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
4
typings/index.d.ts
vendored
@@ -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
11
typings/snapshot.d.ts
vendored
@@ -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
1
typings/types.d.ts
vendored
@@ -86,3 +86,4 @@ export declare type SlimDOMOptions = Partial<{
|
||||
headMetaAuthorship: boolean;
|
||||
headMetaVerification: boolean;
|
||||
}>;
|
||||
export declare type MaskTextFn = (text: string) => string;
|
||||
|
||||
Reference in New Issue
Block a user