Skip to content

Commit a0d8258

Browse files
authored
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
1 parent 3c2d7c1 commit a0d8258

File tree

8 files changed

+127
-7
lines changed

8 files changed

+127
-7
lines changed

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import snapshot, {
33
transformAttribute,
44
visitSnapshot,
55
cleanupSnapshot,
6+
needMaskingText,
67
IGNORED_NODE,
78
} from './snapshot';
89
import rebuild, { buildNodeWithSN, addHoverClass } from './rebuild';
@@ -18,5 +19,6 @@ export {
1819
transformAttribute,
1920
visitSnapshot,
2021
cleanupSnapshot,
22+
needMaskingText,
2123
IGNORED_NODE,
2224
};

src/snapshot.ts

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
idNodeMap,
88
MaskInputOptions,
99
SlimDOMOptions,
10+
MaskTextFn,
1011
} from './types';
1112
import { isElement, isShadowRoot } from './utils';
1213

@@ -206,6 +207,40 @@ export function _isBlockedElement(
206207
return false;
207208
}
208209

210+
export function needMaskingText(
211+
node: Node | null,
212+
maskTextClass: string | RegExp,
213+
maskTextSelector: string | null,
214+
): boolean {
215+
if (!node) {
216+
return false;
217+
}
218+
if (node.nodeType === node.ELEMENT_NODE) {
219+
if (typeof maskTextClass === 'string') {
220+
if ((node as HTMLElement).classList.contains(maskTextClass)) {
221+
return true;
222+
}
223+
} else {
224+
(node as HTMLElement).classList.forEach((className) => {
225+
if (maskTextClass.test(className)) {
226+
return true;
227+
}
228+
});
229+
}
230+
if (maskTextSelector) {
231+
if ((node as HTMLElement).matches(maskTextSelector)) {
232+
return true;
233+
}
234+
}
235+
return needMaskingText(node.parentNode, maskTextClass, maskTextSelector);
236+
}
237+
if (node.nodeType === node.TEXT_NODE) {
238+
// check parent node since text node do not have class name
239+
return needMaskingText(node.parentNode, maskTextClass, maskTextSelector);
240+
}
241+
return needMaskingText(node.parentNode, maskTextClass, maskTextSelector);
242+
}
243+
209244
// https://stackoverflow.com/a/36155560
210245
function onceIframeLoaded(
211246
iframeEl: HTMLIFrameElement,
@@ -259,17 +294,23 @@ function serializeNode(
259294
doc: Document;
260295
blockClass: string | RegExp;
261296
blockSelector: string | null;
297+
maskTextClass: string | RegExp;
298+
maskTextSelector: string | null;
262299
inlineStylesheet: boolean;
263300
maskInputOptions: MaskInputOptions;
301+
maskTextFn: MaskTextFn | undefined;
264302
recordCanvas: boolean;
265303
},
266304
): serializedNode | false {
267305
const {
268306
doc,
269307
blockClass,
270308
blockSelector,
309+
maskTextClass,
310+
maskTextSelector,
271311
inlineStylesheet,
272312
maskInputOptions = {},
313+
maskTextFn,
273314
recordCanvas,
274315
} = options;
275316
// Only record root id when document object is not the base document
@@ -412,12 +453,23 @@ function serializeNode(
412453
n.parentNode && (n.parentNode as HTMLElement).tagName;
413454
let textContent = (n as Text).textContent;
414455
const isStyle = parentTagName === 'STYLE' ? true : undefined;
456+
const isScript = parentTagName === 'SCRIPT' ? true : undefined;
415457
if (isStyle && textContent) {
416458
textContent = absoluteToStylesheet(textContent, getHref());
417459
}
418-
if (parentTagName === 'SCRIPT') {
460+
if (isScript) {
419461
textContent = 'SCRIPT_PLACEHOLDER';
420462
}
463+
if (
464+
!isStyle &&
465+
!isScript &&
466+
needMaskingText(n, maskTextClass, maskTextSelector) &&
467+
textContent
468+
) {
469+
textContent = maskTextFn
470+
? maskTextFn(textContent)
471+
: textContent.replace(/[\S]/g, '*');
472+
}
421473
return {
422474
type: NodeType.Text,
423475
textContent: textContent || '',
@@ -540,9 +592,12 @@ export function serializeNodeWithId(
540592
map: idNodeMap;
541593
blockClass: string | RegExp;
542594
blockSelector: string | null;
595+
maskTextClass: string | RegExp;
596+
maskTextSelector: string | null;
543597
skipChild: boolean;
544598
inlineStylesheet: boolean;
545599
maskInputOptions?: MaskInputOptions;
600+
maskTextFn: MaskTextFn | undefined;
546601
slimDOMOptions: SlimDOMOptions;
547602
recordCanvas?: boolean;
548603
preserveWhiteSpace?: boolean;
@@ -556,9 +611,12 @@ export function serializeNodeWithId(
556611
map,
557612
blockClass,
558613
blockSelector,
614+
maskTextClass,
615+
maskTextSelector,
559616
skipChild = false,
560617
inlineStylesheet = true,
561618
maskInputOptions = {},
619+
maskTextFn,
562620
slimDOMOptions,
563621
recordCanvas = false,
564622
onSerialize,
@@ -570,8 +628,11 @@ export function serializeNodeWithId(
570628
doc,
571629
blockClass,
572630
blockSelector,
631+
maskTextClass,
632+
maskTextSelector,
573633
inlineStylesheet,
574634
maskInputOptions,
635+
maskTextFn,
575636
recordCanvas,
576637
});
577638
if (!_serializedNode) {
@@ -628,9 +689,12 @@ export function serializeNodeWithId(
628689
map,
629690
blockClass,
630691
blockSelector,
692+
maskTextClass,
693+
maskTextSelector,
631694
skipChild,
632695
inlineStylesheet,
633696
maskInputOptions,
697+
maskTextFn,
634698
slimDOMOptions,
635699
recordCanvas,
636700
preserveWhiteSpace,
@@ -675,9 +739,12 @@ export function serializeNodeWithId(
675739
map,
676740
blockClass,
677741
blockSelector,
742+
maskTextClass,
743+
maskTextSelector,
678744
skipChild: false,
679745
inlineStylesheet,
680746
maskInputOptions,
747+
maskTextFn,
681748
slimDOMOptions,
682749
recordCanvas,
683750
preserveWhiteSpace,
@@ -702,11 +769,14 @@ function snapshot(
702769
n: Document,
703770
options?: {
704771
blockClass?: string | RegExp;
772+
blockSelector?: string | null;
773+
maskTextClass?: string | RegExp;
774+
maskTextSelector?: string | null;
705775
inlineStylesheet?: boolean;
706776
maskAllInputs?: boolean | MaskInputOptions;
777+
maskTextFn?: MaskTextFn;
707778
slimDOM?: boolean | SlimDOMOptions;
708779
recordCanvas?: boolean;
709-
blockSelector?: string | null;
710780
preserveWhiteSpace?: boolean;
711781
onSerialize?: (n: INode) => unknown;
712782
onIframeLoad?: (iframeINode: INode, node: serializedNodeWithId) => unknown;
@@ -715,10 +785,13 @@ function snapshot(
715785
): [serializedNodeWithId | null, idNodeMap] {
716786
const {
717787
blockClass = 'rr-block',
788+
blockSelector = null,
789+
maskTextClass = 'rr-mask',
790+
maskTextSelector = null,
718791
inlineStylesheet = true,
719792
recordCanvas = false,
720-
blockSelector = null,
721793
maskAllInputs = false,
794+
maskTextFn,
722795
slimDOM = false,
723796
preserveWhiteSpace,
724797
onSerialize,
@@ -772,9 +845,12 @@ function snapshot(
772845
map: idNodeMap,
773846
blockClass,
774847
blockSelector,
848+
maskTextClass,
849+
maskTextSelector,
775850
skipChild: false,
776851
inlineStylesheet,
777852
maskInputOptions,
853+
maskTextFn,
778854
slimDOMOptions,
779855
recordCanvas,
780856
preserveWhiteSpace,

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,5 @@ export type SlimDOMOptions = Partial<{
105105
headMetaAuthorship: boolean;
106106
headMetaVerification: boolean;
107107
}>;
108+
109+
export type MaskTextFn = (text: string) => string;

test/__snapshots__/integration.ts.snap

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,21 @@ exports[`[html file]: invalid-tagname.html 1`] = `
202202
</body></html>"
203203
`;
204204

205+
exports[`[html file]: mask-text.html 1`] = `
206+
"<!DOCTYPE html><html xmlns=\\"http://www.w3.org/1999/xhtml\\" lang=\\"en\\"><head>
207+
<meta charset=\\"UTF-8\\" />
208+
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\" />
209+
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\" />
210+
<title>Document</title>
211+
</head> <body>
212+
<p class=\\"rr-mask\\">**** *</p>
213+
<div class=\\"rr-mask\\">
214+
<span>**** *</span>
215+
</div>
216+
<div class=\\"rr-mask\\">**** *</div>
217+
</body></html>"
218+
`;
219+
205220
exports[`[html file]: picture.html 1`] = `
206221
"<html xmlns=\\"http://www.w3.org/1999/xhtml\\"><head></head><body>
207222
<picture>

test/html/mask-text.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
7+
<title>Document</title>
8+
</head>
9+
10+
<body>
11+
<p class="rr-mask">mask 1</p>
12+
<div class="rr-mask">
13+
<span>mask 2</span>
14+
</div>
15+
<div class="rr-mask">mask 3</div>
16+
</body>
17+
</html>

typings/index.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import snapshot, { serializeNodeWithId, transformAttribute, visitSnapshot, cleanupSnapshot, IGNORED_NODE } from './snapshot';
1+
import snapshot, { serializeNodeWithId, transformAttribute, visitSnapshot, cleanupSnapshot, needMaskingText, IGNORED_NODE } from './snapshot';
22
import rebuild, { buildNodeWithSN, addHoverClass } from './rebuild';
33
export * from './types';
44
export * from './utils';
5-
export { snapshot, serializeNodeWithId, rebuild, buildNodeWithSN, addHoverClass, transformAttribute, visitSnapshot, cleanupSnapshot, IGNORED_NODE, };
5+
export { snapshot, serializeNodeWithId, rebuild, buildNodeWithSN, addHoverClass, transformAttribute, visitSnapshot, cleanupSnapshot, needMaskingText, IGNORED_NODE, };

typings/snapshot.d.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
import { serializedNodeWithId, INode, idNodeMap, MaskInputOptions, SlimDOMOptions } from './types';
1+
import { serializedNodeWithId, INode, idNodeMap, MaskInputOptions, MaskTextFn, SlimDOMOptions } from './types';
22
export declare const IGNORED_NODE = -2;
33
export declare function absoluteToStylesheet(cssText: string | null, href: string): string;
44
export declare function absoluteToDoc(doc: Document, attributeValue: string): string;
55
export declare function transformAttribute(doc: Document, tagName: string, name: string, value: string): string;
66
export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null): boolean;
7+
export declare function needMaskingText(node: Node | null, maskTextClass: string | RegExp, maskTextSelector: string | null): boolean;
78
export declare function serializeNodeWithId(n: Node | INode, options: {
89
doc: Document;
910
map: idNodeMap;
1011
blockClass: string | RegExp;
1112
blockSelector: string | null;
13+
maskTextClass: string | RegExp;
14+
maskTextSelector: string | null;
1215
skipChild: boolean;
1316
inlineStylesheet: boolean;
1417
maskInputOptions?: MaskInputOptions;
18+
maskTextFn?: MaskTextFn;
1519
slimDOMOptions: SlimDOMOptions;
1620
recordCanvas?: boolean;
1721
preserveWhiteSpace?: boolean;
@@ -21,11 +25,14 @@ export declare function serializeNodeWithId(n: Node | INode, options: {
2125
}): serializedNodeWithId | null;
2226
declare function snapshot(n: Document, options?: {
2327
blockClass?: string | RegExp;
28+
blockSelector?: string | null;
29+
maskTextClass?: string | RegExp;
30+
maskTextSelector?: string | null;
2431
inlineStylesheet?: boolean;
2532
maskAllInputs?: boolean | MaskInputOptions;
33+
maskTextFn?: MaskTextFn;
2634
slimDOM?: boolean | SlimDOMOptions;
2735
recordCanvas?: boolean;
28-
blockSelector?: string | null;
2936
preserveWhiteSpace?: boolean;
3037
onSerialize?: (n: INode) => unknown;
3138
onIframeLoad?: (iframeINode: INode, node: serializedNodeWithId) => unknown;

typings/types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,4 @@ export declare type SlimDOMOptions = Partial<{
8686
headMetaAuthorship: boolean;
8787
headMetaVerification: boolean;
8888
}>;
89+
export declare type MaskTextFn = (text: string) => string;

0 commit comments

Comments
 (0)