-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathstringReplaceStream.ts
More file actions
118 lines (104 loc) · 2.8 KB
/
stringReplaceStream.ts
File metadata and controls
118 lines (104 loc) · 2.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import { Transform, TransformCallback } from 'stream';
import escapeStringRegexp from 'escape-string-regexp';
/** The replacer string or function passed to string.replace(text: string, replacer: *MatchReplacement*) */
export type MatchReplacement =
| string
| ((matchedSubstring: string, ...capturedGroups: string[]) => string);
type Replacer = {
matcher: RegExp;
replace: MatchReplacement;
};
type Options = {
encoding: BufferEncoding;
ignoreCase: boolean;
useRegExp: boolean;
};
const defaultOptions: Options = {
encoding: 'utf8',
ignoreCase: true,
useRegExp: false,
};
function buildReplacers(
replacements: Record<string, MatchReplacement>,
opts: Options
): Replacer[] {
return Object.keys(replacements)
.sort((a, b) => b.length - a.length)
.map(search => ({
matcher: new RegExp(
opts.useRegExp ? search : escapeStringRegexp(search),
opts.ignoreCase ? 'gmi' : 'gm'
),
replace: replacements[search],
}));
}
function getMaxSearchLength(
replacements: Record<string, MatchReplacement>
): number {
return Object.keys(replacements).reduce(
(acc, search) => Math.max(acc, search.length),
0
);
}
export default function StringReplaceStream(
replacements: Record<string, MatchReplacement>,
options: Partial<Options> = {}
) {
const opts: Options = { ...defaultOptions, ...options };
const replacers = buildReplacers(replacements, opts);
const maxSearchLength = getMaxSearchLength(replacements);
let tail = '';
const replaceSlidingWindow = (
haystack: string,
replacers: Replacer[],
replaceBefore: number
) => {
/**
* foo => foo123
* foo ba | r ba
* foo123 ba | r baz
* foo123 | bar baz
*
* foo => f
* foo bar baz => f bar baz
*/
let body = haystack;
replacers.forEach(replacer => {
body =
body
.slice(0, replaceBefore)
.replace(replacer.matcher, replacer.replace as string) +
body.slice(replaceBefore);
});
return [body.slice(0, replaceBefore), body.slice(replaceBefore)];
};
const transform = function(
buf: Buffer,
_enc: BufferEncoding,
cb: TransformCallback
) {
const replaceBefore = maxSearchLength * 2;
const haystack = tail + buf.toString(opts.encoding);
let body = '';
if (haystack.length < maxSearchLength * 3 - 2) {
tail = haystack;
cb(null, '');
return;
}
[body, tail] = replaceSlidingWindow(haystack, replacers, replaceBefore);
cb(null, body);
};
const flush = function(cb: TransformCallback) {
if (!tail) {
cb();
return;
}
const body = replacers.reduce(
(acc, replacer) =>
acc.replace(replacer.matcher, replacer.replace as string),
tail
);
cb(null, body);
};
return new Transform({ transform, flush });
}