-
Notifications
You must be signed in to change notification settings - Fork 5
/
index.js
406 lines (357 loc) · 23.7 KB
/
index.js
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
const strict = "strict", strictOption = option => option === strict && { strict: true };
/**
* Options to minify an XML document.
*
* @typedef {object} MinifyOptions
* @property {boolean} removeComments Remove XML comments <!-- ... -->
* @property {boolean|string} removeWhitespaceBetweenTags Remove whitespace only between tags <anyTag/> <anyOtherTag/> (true / false or 'strict', strict will not consider prolog / doctype, as tags)
* @property {boolean} considerPreserveWhitespace Remove / trim whitespace in texts like <anyTag> foo </anyTag>
* @property {boolean} collapseWhitespaceInTags Remove / collapse whitespace in tags <anyTag attributeA = "..." attributeB = "..."> ... </anyTag >
* @property {boolean} collapseEmptyElements Collapse elements with start / end tags and no content to empty element tags <anyTag anyAttribute = "..." ></anyTag >
* @property {boolean|string} trimWhitespaceFromTexts Remove / trim whitespace in texts like <anyTag> foo </anyTag> (true / false or 'strict', strict will not consider prolog / doctype, as tags)
* @property {boolean|string} collapseWhitespaceInTexts Collapse whitespace in texts like <anyTag>foo bar baz</anyTag> (true / false or 'strict', strict will not consider prolog / doctype, as tags)
* @property {boolean} collapseWhitespaceInProlog Remove / collapse whitespace in the xml prolog <?xml version = "1.0" ?>
* @property {boolean} collapseWhitespaceInDocType Remove / collapse whitespace in the xml document type declaration <!DOCTYPE DocType >
* @property {boolean} removeSchemaLocationAttributes Remove any xsi:schemaLocation / xsi:noNamespaceSchemaLocation attributes <anyTag xsi:schemaLocation="/schema/" />
* @property {boolean} removeUnnecessaryStandaloneDeclaration Remove unnecessary standalone declaration in prolog <?xml standalone = "yes" ?>
* @property {boolean} removeUnusedNamespaces Remove unused namespaces and shorten the remaining ones to a minimum length
* @property {boolean} removeUnusedDefaultNamespace Remove unused default namespace declaration if no tags with no namespace declaration are present
* @property {boolean} shortenNamespaces Shorten existing (non already one character namespaces) to a shorter equivalent
* @property {boolean} ignoreCData Ignore CDATA sections <![CDATA[ ... ]]>
*/
/**
* The default options applied when minifying an XML document.
*
* @type {MinifyOptions}
*/
export const defaultOptions = {
removeComments: true,
removeWhitespaceBetweenTags: true, // true / false or 'strict' (will not consider prolog / doctype, as tags)
considerPreserveWhitespace: true,
collapseWhitespaceInTags: true,
collapseEmptyElements: true,
trimWhitespaceFromTexts: false, // true / false or 'strict'
collapseWhitespaceInTexts: false, // true / false or 'strict'
collapseWhitespaceInProlog: true,
collapseWhitespaceInDocType: true,
removeSchemaLocationAttributes: false,
removeUnnecessaryStandaloneDeclaration: true,
removeUnusedNamespaces: true,
removeUnusedDefaultNamespace: true,
shortenNamespaces: true,
ignoreCData: true
};
function trim(string) {
return string.replace(/^[\s\uFEFF\xA0]+/g, String()).replace(/[\s\uFEFF\xA0]+$/g, String());
}
const emptyRegExp = new RegExp(), emptyPattern = emptyRegExp.source, regExpGlobal = "g";
function escapeRegExp(string) {
return string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
function findAllMatches(string, regexp, group) {
let matches = [], match;
while ((match = regexp.exec(string))) {
if (typeof group === "number") {
match[group] && matches.push(match[group]);
} else {
matches.push(match);
}
} return matches;
}
// note: this funky looking positive lookbehind regular expression is necessary to match contents inside of tags <...>. this
// is due to that literally any characters except <&" are allowed to be put next to everywhere in XML. as even > is an allowed
// character, simply checking for (?<=<[^>]*) would not do the trick if e.g. > is used inside of a tag attribute.
const tagPattern = /(?<=<\/?[^?!\s\/>]+\b(?:\s+[^=\s>]+\s*=\s*(?:"[^"]*"|'[^']*'))*%1)/.source, noTagPattern = /[^<]*/.source,
bracketPattern = tagPattern.replace(/(?<!\(\?)</, "<(?:" + /!\s*(?:--(?:[^-]|-[^-])*--\s*)|!\[(?:CDATA|.*?)\[(?:[^\]]|][^\]]|]][^>])*]]|!DOCTYPE\s+(?:[^>[]|\[[^\]]*\])*|\?[^>]*|/.source).replace("%1", ")%1"),
prologPattern = tagPattern.replace(/(?<=(?<!\(\?)<).*(?=\\b)/, "\\?xml"),
docTypePattern = /<!DOCTYPE\s+([^\s>[]+)(?:\s+(SYSTEM|PUBLIC)\s+("[^"]*"|'[^']*')(?:\s+("[^"]*"|'[^']*'))?)?(?:\s*\[([^\]]*)\])?\s*>/.source,
preservePattern = /(?<!<(?:[^\s\/>:]+:)?pre[^<]*?>|\s+xml:space\s*=\s*(?:"preserve"|'preserve'|preserve)(?:\s+[^=\s>]+\s*=\s*(?:"[^"]*"|'[^']*'))*\s*>)/.source;
function findAllMatchesInTags(xml, regexp, options = { tagPattern, lookbehind: emptyRegExp, lookbehindPattern: String(), group: 0 }) {
const lookbehindPattern = options.lookbehindPattern || (options.lookbehind || emptyRegExp).source;
return findAllMatches(xml, new RegExp((options.tagPattern || tagPattern).replace("%1", lookbehindPattern) + regexp.source, regExpGlobal), options.group);
}
// include non-tags means declaration like <! comments / doctype declaration and <? prolog / processing instructions
function replaceInTags(xml, regexp, replacement, options = { tagPattern, lookbehind: emptyRegExp, lookbehindPattern: String() }) {
const lookbehindPattern = options.lookbehindPattern || (options.lookbehind || emptyRegExp).source;
return xml.replace(new RegExp((options.tagPattern || tagPattern).replace("%1", lookbehindPattern) + regexp.source, regExpGlobal), replacement);
}
const defaultReplaceBetweenOptions = { lookbehind: emptyRegExp, lookbehindPattern: String(), lookahead: emptyRegExp, lookaheadPattern: String() };
function replaceBetweenTags(xml, regexp, replacement, options = defaultReplaceBetweenOptions) {
const lookbehindPattern = "\\s*/?>" + (options.lookbehindPattern || (options.lookbehind || emptyRegExp).source),
lookaheadPattern = (options.lookaheadPattern || (options.lookahead || emptyRegExp).source) + "<[^?!]";
return replaceInTags(xml, new RegExp(regexp.source + `(?=${ lookaheadPattern })`), replacement, { lookbehindPattern });
}
function replaceBetweenBrackets(xml, regexp, replacement, options = defaultReplaceBetweenOptions) {
const lookbehindPattern = "\\s*[!?/]?>" + (options.lookbehindPattern || (options.lookbehind || emptyRegExp).source),
lookaheadPattern = (options.lookaheadPattern || (options.lookahead || emptyRegExp).source) + "<";
return replaceInTags(xml, new RegExp(regexp.source + `(?=${ lookaheadPattern })`), replacement, { tagPattern: bracketPattern, lookbehindPattern });
}
function replaceBetween(xml, regexp, replacement, options = { ...defaultOptions, strict: false }) {
// if not strict also consider the prolog <?xml ... ?>, processing instructions <?pi ... ?>, the document type declaration <!DOCTYPE ... >, CDATA sections <![CDATA[ ... ]]> and comments <!-- ... --> as tags here
return (options.strict ? replaceBetweenTags : replaceBetweenBrackets)(xml, regexp, replacement, options);
}
function ignoreCData(replacement) {
return function(match, offset, string, groups) {
// the interface of replacement functions contains any number of arguments at the second position, for contents of capturing groups.
// the last argument is either an object (for browsers supporting named capturing groups) or the examined string otherwise.
let argument = arguments.length - 1, captures;
groups = typeof arguments[argument] === "object" ? arguments[argument--] : undefined;
string = arguments[argument--]; offset = arguments[argument--];
captures = Array.prototype.slice.call(arguments, 1, argument + 1);
// check if the offset lies inside of a CData section
if (/<!\[CDATA\[(?![\s\S]*?]]>)/.test(string.substring(0, offset))) {
return match; // if so do not replace anything
}
// if the replacement is a function, apply our arguments
if (typeof replacement === "function") {
return replacement.apply(this, arguments);
}
// otherwise execute the replacement of the capturing groups manually
return captures ? replacement.replace(/(?<!\$)\$(\d+|\&)/g, (group, number) =>
["0", "&"].includes(number) ? match : captures[parseInt(number - 1)] || String()) : replacement;
};
}
/**
* Minify an XML document.
*
* @param {string} xml The XML document to minify
* @param {MinifyOptions} [options=defaultOptions] The options to minify the XML document
* @returns {string} The minified XML document
*/
export function minify(xml, options) {
// apply the default options
options = {
...defaultOptions,
...(options || {})
};
// decide on whether to use the ignoreCData replacement function or not, to improve performance
const replacer = options.ignoreCData && xml.includes("<![CDATA[") ? ignoreCData : replacement => replacement, emptyReplacer = replacer(String());
function removeComments(xml) {
return xml.replace(/<!\s*(?:--(?:[^-]|-[^-])*--\s*)>/g, emptyReplacer);
}
// remove XML comments <!-- ... -->
if (options.removeComments) {
xml = removeComments(xml);
}
// remove whitespace only between tags <anyTag/> <anyOtherTag/>
if (options.removeWhitespaceBetweenTags) {
xml = replaceBetween(xml, /\s+/, emptyReplacer, strictOption(options.removeWhitespaceBetweenTags));
}
function collapseWhitespaceInTags(xml, options = { tagPattern }) {
xml = replaceInTags(xml, /\s+/, replacer(" "), options); // collapse whitespace between attributes
xml = replaceInTags(xml, /\s*=\s*/, replacer("="), { ...options, lookbehind: /\s+[^=\s>]+/ }); // remove leading / tailing whitespace around attribute equal signs
xml = replaceInTags(xml, /\s+(?=[/?]?>)/, emptyReplacer, options); // remove whitespace before closing > /> ?> of tags
return xml;
}
// remove any xsi:schemaLocation / xsi:noNamespaceSchemaLocation attributes <anyTag xsi:schemaLocation="/schema/" />
if (options.removeSchemaLocationAttributes) {
xml = replaceInTags(xml, /\s+xsi:(?:noNamespaceS|s)chemaLocation\s*=\s*(?:"[^"]*"|'[^']*')/, replacer(" "));
}
// remove / collapse whitespace in tags <anyTag attributeA = "..." attributeB = "..."> ... </anyTag >
if (options.collapseWhitespaceInTags) {
xml = collapseWhitespaceInTags(xml);
}
// collapse elements with start / end tags and no content to empty element tags <anyTag anyAttribute = "..." ></anyTag >
if (options.collapseEmptyElements) {
xml = xml.replace(/<([^\s\/>]+)([^<]*?)(?<!\/)><\/\1\s*>/g, replacer("<$1$2/>"));
}
// remove / trim whitespace in texts like <anyTag> foo </anyTag>
if (options.trimWhitespaceFromTexts) {
// note, to avoid zero-length matches use two replaceBetween here (a zero-length match causes an endless loop in replacestream)
xml = replaceBetween(xml, /\s+/, emptyReplacer, { lookbehindPattern: options.considerPreserveWhitespace ?
preservePattern : null, lookaheadPattern: noTagPattern, ...strictOption(options.trimWhitespaceFromTexts) });
xml = replaceBetween(xml, /\s+/, emptyReplacer, { lookbehindPattern: (options.considerPreserveWhitespace ?
preservePattern : String()) + noTagPattern, ...strictOption(options.trimWhitespaceFromTexts) });
}
// collapse whitespace in texts like <anyTag>foo bar baz</anyTag>
if (options.collapseWhitespaceInTexts) {
xml = replaceBetween(xml, /\s+/, replacer(" "), { lookbehindPattern: (options.considerPreserveWhitespace ?
preservePattern : emptyPattern ) + noTagPattern, lookaheadPattern: noTagPattern, ...strictOption(options.collapseWhitespaceInTexts) });
}
// remove remove unnecessary standalone declaration in prolog <?xml standalone = "yes" ?>
// the standalone declaration has "no meaning" according to the W3C definition, in case neither the external subset of the DocType declaration
// contains any markup declarations (<!ELEMENT, <!ATTLIST, <!ENTITY, <!NOTATION) or a parameter entity (<!ENTITY %) is defined in the any subset
// (because we do not read the external subset definition file e.g. schema.dtd, we assume as soon as either a SYSTEM/PUBLIC subset is defined, the standalone attribute must stay)
if (options.removeUnnecessaryStandaloneDeclaration) {
const docType = xml.match(new RegExp(docTypePattern));
if (!docType || (!docType[2] && !(docType[5] && /<!ENTITY\s+%/.test(docType[5])))) {
xml = replaceInTags(xml, /\s+standalone\s*=\s*(?:"yes"|'yes'|yes|"no"|'no'|no)/, emptyReplacer, { tagPattern: prologPattern });
}
}
// remove / collapse whitespace in the xml prolog <?xml version = "1.0" ?>
if (options.collapseWhitespaceInProlog) {
xml = collapseWhitespaceInTags(xml, { tagPattern: prologPattern });
}
// remove / collapse whitespace in the xml document type declaration <!DOCTYPE DocType >
if (options.collapseWhitespaceInDocType) {
xml = xml.replace(new RegExp(docTypePattern), replacer(
(match, name, type, literal1, literal2, subset) => `<!DOCTYPE ${name}${ [type, literal1, literal2]
.map(token => token && " " + token).join(String()) }${ subset ? `[${ (xml => {
// use a simplified minify xml for the internal subset declaration of the document type
xml = removeComments(xml); // remove comments
xml = xml.replace(/\s+/g, " "); // collapse whitespace
xml = xml.replace(/>\s+</g, "><"); // remove any whitespace between declarations (assuming that > cannot appear in the declarations themselves)
return xml.trim ? xml.trim() : trim(xml);
})(subset) }]` : String() }>`));
}
// remove unused namespaces and shorten the remaining ones to a minimum length
if (options.removeUnusedNamespaces || options.shortenNamespaces) {
// the search for all xml namespaces in tags could result in some "fake" namespaces if a xmlns:... string is found inside of CDATA
// tags. this however comes with no major drawback as we the replace only inside of tags and thus it simplifies the search
let all = [...new Set(findAllMatchesInTags(xml, /\s+xmlns:([^\s=]+)\s*=/g, { group: 1 }))];
// remove namespace declarations which are not used anywhere in the document (limitation: the approach taken here will not consider the structure of the XML document
// thus namespaces which might be only used in a certain sub-tree of elements might not be removed, even though they are not used in that sub-tree)
if (options.removeUnusedNamespaces) {
let used = [...new Set([
...findAllMatches(xml, /<([^\s\/>:]+):/g, 1), // look for all tags with namespaces (limitation: might also include tags inside of CData, we ignore that for now)
...findAllMatchesInTags(xml, /([^\s=:]+):/, { lookbehind: /\s+/, group: 1 }) // look for all attributes with namespaces
])].filter(ns => ns !== "xmlns"), unused = all.filter(ns => !used.includes(ns));
if (unused.length) {
xml = replaceInTags(xml, new RegExp(`\\s+xmlns:(?:${ unused.map(escapeRegExp).join("|") })\\s*=\\s*(?:"[^"]*"|'[^']*')`), emptyReplacer);
all = used; // only used namespaces still present in the file
}
}
// special case: remove unused default namespace declaration if no tags with no namespace declaration are present
// (it's impossible for attributes with namespaces to refer back to the default namespace, so we can omit searching for them)
if (options.removeUnusedDefaultNamespace && !/<([^\s\/>:]+)[\s\/>]/.test(xml)) {
xml = replaceInTags(xml, /\s+xmlns\s*=\s*(?:"[^"]*"|'[^']*')/, emptyReplacer);
}
// shorten existing (non already one character namespaces) to a shorter equivalent
if(options.shortenNamespaces) {
const startCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_",
charset = startCharset.substring(0, 52) + "0123456789-_.";
function firstUnusedNamespace(prefix, length) {
if (!arguments.length) {
for (length = 1; !(prefix = firstUnusedNamespace(
String(), length)); length++);
return prefix;
} else if (!length) {
return prefix;
}
const chars = prefix ? charset : startCharset;
for (let char = 0; char < chars.length; char++) {
let ns = firstUnusedNamespace(prefix + chars[char], length - 1);
if (ns && !all.includes(ns)) {
return ns;
}
}
return false; // for this length / prefix there is no unused namespace to choose from
}
all.forEach((ns, idx) => {
// never shorten the special "xsi" namespace or if already at absolute minimal length
if (ns === "xsi" || ns.length === 1) {
return;
}
// try to shorten the existing namespace to one character first, if it is occupied already, find the first unused one by brute force
let newNs = !all.includes(ns[0]) ? ns[0] : firstUnusedNamespace();
if (ns.length <= newNs.length) {
return; // already at minimal length
}
// replace all occurrences of the namespace in the document and mark it as "used"
xml = xml.replace(new RegExp(`<(/)?${ns}:`, regExpGlobal), replacer(`<$1${newNs}:`)); // tags with namespaces
xml = replaceInTags(xml, new RegExp(`${ns}:`), replacer(`${newNs}:`), { lookbehind: /\s+/ }); // attributes with namespaces
xml = replaceInTags(xml, new RegExp(`xmlns:${ns}(?=[\\s=])`), replacer(`xmlns:${newNs}`), { lookbehind: /\s+/ }); // namespace declaration
all[idx] = newNs;
});
}
}
return xml.trim ? xml.trim() : trim(xml);
}; export default minify;
import pumpify from "pumpify"; // XXX: to be replaced by node:stream compose as soon as it is stable
import replaceStream from "replacestream"; // note that replacestream does NOT support zero-length regex matches!
import { PassThrough } from "node:stream";
/**
* Options to minify an XML document stream.
*
* @typedef {object} MinifyStreamSpecificOptions
* @property {number} streamMaxMatchLength The maximum size of matches between chunks
*/
/**
* Options to minify an XML document stream.
*
* @typedef {Omit<MinifyOptions, "removeUnnecessaryStandaloneDeclaration" | "removeUnusedNamespaces" | "removeUnusedDefaultNamespace" | "shortenNamespaces" | "ignoreCData"> & MinifyStreamSpecificOptions} MinifyStreamOptions
*/
// some options require prior knowledge, like 'removeUnnecessaryStandaloneDeclaration' will have to read the DocType first and
// 'removeUnusedNamespaces' needs to scan the document for namespaces in use, thus some options cannot be used when streaming
const unsupportedStreamOptions = ["removeUnnecessaryStandaloneDeclaration", "removeUnusedNamespaces", "removeUnusedDefaultNamespace", "shortenNamespaces", "ignoreCData"];
/**
* The default options applied when minifying an XML document stream.
*
* @type {MinifyStreamOptions}
*/
export const defaultStreamOptions = {
...defaultOptions,
streamMaxMatchLength: 256 * 1024, // 256 KiB, maximum size of matches between chunks
// all these options require prior knowledge about the stream, for instance if we are in a CData block, or what namespaces are present
...Object.fromEntries(unsupportedStreamOptions.map(option => [option, false]))
};
/**
* Minify an XML document stream.
*
* @param {MinifyStreamOptions} [options=defaultStreamOptions] The options to minify the XML document stream
* @returns {import('node:stream').Duplex} A duplex stream that minifies an XML document
*/
export function minifyStream(options) {
// apply the default options
options = {
...defaultStreamOptions,
...(options || {})
};
// ignoring CData sections is not supported w/ streams
const unsupportedOption = unsupportedStreamOptions.find(option => !!options[option]);
if (unsupportedOption) {
throw new Error(`The '${unsupportedOption}' option cannot be used with streams, as it requires prior knowledge about the stream to minify`);
}
// the minify function accepts strings only, however as we took care (e.g. by using a polyfill 'trim' function) that the function is only repeatedly
// calling the strings 'replace' function on an unmodified input object, we can take advantage of duck typing here and pass an object resembling a
// very simple string, that captures all calls to 'replace' and maps them into 'replacestream'. all unsupported options, e.g. those requiring prior
// knowledge about the stream like 'removeUnusedNamespaces', must be disabled
const streams = [], replaceOptions = { maxMatchLen: options.streamMaxMatchLength }, stringImposter = {
replace: function() {
streams.push(replaceStream(...arguments, replaceOptions));
return stringImposter;
}
};
// called with the string-like object, it will create a chain of (replace)streams, which, if we pipe data into the first stream, apply all minifications
minify(stringImposter, options);
// minify will always 'trim' the output, if more minification transformations have been applied, pumpify all streams into one
if (streams.length > 1) {
return pumpify(streams);
} else if (streams.length) {
return streams[0].pipe(new PassThrough()) // bug: replacestream returns an old transform stream that is not async. iterable, we fix that by piping it through a PassThrough stream ourselves
} else {
return new PassThrough();
}
};
import { pipeline } from "node:stream/promises";
/**
* Minify an XML document pipeline.
*
* @param {import("node:stream").PipelineSource<string>} source The source of the XML document pipeline
* @param {import("node:stream").PipelineDestination<import("node:stream").PipelineTransformSource<string>, string>} destination The destination of the XML document pipeline
* @param {MinifyStreamOptions} [options=defaultStreamOptions] The options to minify the XML document pipeline
* @returns {import("node:stream").PipelinePromise<import("node:stream").PipelineDestination<any, string>>} A promise that resolves into the destination of the XML document pipeline
*/
export const minifyPipeline = async (source, destination, options) =>
await pipeline(source, minifyStream(options), destination, { end: options?.end });
/**
* Debug minifying an XML document.
*
* @ignore
* @param {string} xml The XML document to debug minifying
* @param {MinifyOptions} [options=defaultOptions] The options to minify the XML document
*/
export function debug(xml, options) {
xml && console.log(`\x1b[90m${xml}\x1b[0m`);
// the minify function accepts strings only, however only 'replace' is being called repeatedly, so we can take advantage of duck typing here
const stringImposter = {
includes: (...args) => xml ? xml.includes(...args) : true,
replace: function(...args) {
console.log(`\x1b[31m${args[0]}\x1b[0m`);
xml && console.log(`\x1b[90m${xml = xml.replace(...args)}\x1b[0m`);
return stringImposter;
}
};
// called with the string-like object, to dump all regular expressions into the console
minify(stringImposter, options);
};