UNPKG

12.6 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5"use strict";
6
7const { getMap, getSourceAndMap } = require("./helpers/getFromStreamChunks");
8const streamChunks = require("./helpers/streamChunks");
9const Source = require("./Source");
10
11// since v8 7.0, Array.prototype.sort is stable
12const hasStableSort =
13 typeof process === "object" &&
14 process.versions &&
15 typeof process.versions.v8 === "string" &&
16 !/^[0-6]\./.test(process.versions.v8);
17
18// This is larger than max string length
19const MAX_SOURCE_POSITION = 0x20000000;
20
21const SPLIT_LINES_REGEX = /[^\n]+\n?|\n/g;
22
23class Replacement {
24 constructor(start, end, content, name) {
25 this.start = start;
26 this.end = end;
27 this.content = content;
28 this.name = name;
29 if (!hasStableSort) {
30 this.index = -1;
31 }
32 }
33}
34
35class ReplaceSource extends Source {
36 constructor(source, name) {
37 super();
38 this._source = source;
39 this._name = name;
40 /** @type {Replacement[]} */
41 this._replacements = [];
42 this._isSorted = true;
43 }
44
45 getName() {
46 return this._name;
47 }
48
49 getReplacements() {
50 this._sortReplacements();
51 return this._replacements;
52 }
53
54 replace(start, end, newValue, name) {
55 if (typeof newValue !== "string")
56 throw new Error(
57 "insertion must be a string, but is a " + typeof newValue
58 );
59 this._replacements.push(new Replacement(start, end, newValue, name));
60 this._isSorted = false;
61 }
62
63 insert(pos, newValue, name) {
64 if (typeof newValue !== "string")
65 throw new Error(
66 "insertion must be a string, but is a " +
67 typeof newValue +
68 ": " +
69 newValue
70 );
71 this._replacements.push(new Replacement(pos, pos - 1, newValue, name));
72 this._isSorted = false;
73 }
74
75 source() {
76 if (this._replacements.length === 0) {
77 return this._source.source();
78 }
79 let current = this._source.source();
80 let pos = 0;
81 const result = [];
82
83 this._sortReplacements();
84 for (const replacement of this._replacements) {
85 const start = Math.floor(replacement.start);
86 const end = Math.floor(replacement.end + 1);
87 if (pos < start) {
88 const offset = start - pos;
89 result.push(current.slice(0, offset));
90 current = current.slice(offset);
91 pos = start;
92 }
93 result.push(replacement.content);
94 if (pos < end) {
95 const offset = end - pos;
96 current = current.slice(offset);
97 pos = end;
98 }
99 }
100 result.push(current);
101 return result.join("");
102 }
103
104 map(options) {
105 if (this._replacements.length === 0) {
106 return this._source.map(options);
107 }
108 return getMap(this, options);
109 }
110
111 sourceAndMap(options) {
112 if (this._replacements.length === 0) {
113 return this._source.sourceAndMap(options);
114 }
115 return getSourceAndMap(this, options);
116 }
117
118 original() {
119 return this._source;
120 }
121
122 _sortReplacements() {
123 if (this._isSorted) return;
124 if (hasStableSort) {
125 this._replacements.sort(function (a, b) {
126 const diff1 = a.start - b.start;
127 if (diff1 !== 0) return diff1;
128 const diff2 = a.end - b.end;
129 if (diff2 !== 0) return diff2;
130 return 0;
131 });
132 } else {
133 this._replacements.forEach((repl, i) => (repl.index = i));
134 this._replacements.sort(function (a, b) {
135 const diff1 = a.start - b.start;
136 if (diff1 !== 0) return diff1;
137 const diff2 = a.end - b.end;
138 if (diff2 !== 0) return diff2;
139 return a.index - b.index;
140 });
141 }
142 this._isSorted = true;
143 }
144
145 streamChunks(options, onChunk, onSource, onName) {
146 this._sortReplacements();
147 const repls = this._replacements;
148 let pos = 0;
149 let i = 0;
150 let replacmentEnd = -1;
151 let nextReplacement =
152 i < repls.length ? Math.floor(repls[i].start) : MAX_SOURCE_POSITION;
153 let generatedLineOffset = 0;
154 let generatedColumnOffset = 0;
155 let generatedColumnOffsetLine = 0;
156 const sourceContents = [];
157 const nameMapping = new Map();
158 const nameIndexMapping = [];
159 const checkOriginalContent = (sourceIndex, line, column, expectedChunk) => {
160 let content =
161 sourceIndex < sourceContents.length
162 ? sourceContents[sourceIndex]
163 : undefined;
164 if (content === undefined) return false;
165 if (typeof content === "string") {
166 content = content.match(SPLIT_LINES_REGEX) || [];
167 sourceContents[sourceIndex] = content;
168 }
169 const contentLine = line <= content.length ? content[line - 1] : null;
170 if (contentLine === null) return false;
171 return (
172 contentLine.slice(column, column + expectedChunk.length) ===
173 expectedChunk
174 );
175 };
176 let { generatedLine, generatedColumn } = streamChunks(
177 this._source,
178 Object.assign({}, options, { finalSource: false }),
179 (
180 chunk,
181 generatedLine,
182 generatedColumn,
183 sourceIndex,
184 originalLine,
185 originalColumn,
186 nameIndex
187 ) => {
188 let chunkPos = 0;
189 let endPos = pos + chunk.length;
190
191 // Skip over when it has been replaced
192 if (replacmentEnd > pos) {
193 // Skip over the whole chunk
194 if (replacmentEnd >= endPos) {
195 const line = generatedLine + generatedLineOffset;
196 if (chunk.endsWith("\n")) {
197 generatedLineOffset--;
198 if (generatedColumnOffsetLine === line) {
199 // undo exiting corrections form the current line
200 generatedColumnOffset += generatedColumn;
201 }
202 } else if (generatedColumnOffsetLine === line) {
203 generatedColumnOffset -= chunk.length;
204 } else {
205 generatedColumnOffset = -chunk.length;
206 generatedColumnOffsetLine = line;
207 }
208 pos = endPos;
209 return;
210 }
211
212 // Partially skip over chunk
213 chunkPos = replacmentEnd - pos;
214 if (
215 checkOriginalContent(
216 sourceIndex,
217 originalLine,
218 originalColumn,
219 chunk.slice(0, chunkPos)
220 )
221 ) {
222 originalColumn += chunkPos;
223 }
224 pos += chunkPos;
225 const line = generatedLine + generatedLineOffset;
226 if (generatedColumnOffsetLine === line) {
227 generatedColumnOffset -= chunkPos;
228 } else {
229 generatedColumnOffset = -chunkPos;
230 generatedColumnOffsetLine = line;
231 }
232 generatedColumn += chunkPos;
233 }
234
235 // Is a replacement in the chunk?
236 if (nextReplacement < endPos) {
237 do {
238 let line = generatedLine + generatedLineOffset;
239 if (nextReplacement > pos) {
240 // Emit chunk until replacement
241 const offset = nextReplacement - pos;
242 const chunkSlice = chunk.slice(chunkPos, chunkPos + offset);
243 onChunk(
244 chunkSlice,
245 line,
246 generatedColumn +
247 (line === generatedColumnOffsetLine
248 ? generatedColumnOffset
249 : 0),
250 sourceIndex,
251 originalLine,
252 originalColumn,
253 nameIndex < 0 ? -1 : nameIndexMapping[nameIndex]
254 );
255 generatedColumn += offset;
256 chunkPos += offset;
257 pos = nextReplacement;
258 if (
259 checkOriginalContent(
260 sourceIndex,
261 originalLine,
262 originalColumn,
263 chunkSlice
264 )
265 ) {
266 originalColumn += chunkSlice.length;
267 }
268 }
269
270 // Insert replacement content splitted into chunks by lines
271 const regexp = /[^\n]+\n?|\n/g;
272 const { content, name } = repls[i];
273 let match = regexp.exec(content);
274 let replacementNameIndex = nameIndex;
275 if (sourceIndex >= 0 && name) {
276 let globalIndex = nameMapping.get(name);
277 if (globalIndex === undefined) {
278 globalIndex = nameMapping.size;
279 nameMapping.set(name, globalIndex);
280 onName(globalIndex, name);
281 }
282 replacementNameIndex = globalIndex;
283 }
284 while (match !== null) {
285 const contentLine = match[0];
286 onChunk(
287 contentLine,
288 line,
289 generatedColumn +
290 (line === generatedColumnOffsetLine
291 ? generatedColumnOffset
292 : 0),
293 sourceIndex,
294 originalLine,
295 originalColumn,
296 replacementNameIndex
297 );
298
299 // Only the first chunk has name assigned
300 replacementNameIndex = -1;
301
302 match = regexp.exec(content);
303 if (match === null && !contentLine.endsWith("\n")) {
304 if (generatedColumnOffsetLine === line) {
305 generatedColumnOffset += contentLine.length;
306 } else {
307 generatedColumnOffset = contentLine.length;
308 generatedColumnOffsetLine = line;
309 }
310 } else {
311 generatedLineOffset++;
312 line++;
313 generatedColumnOffset = -generatedColumn;
314 generatedColumnOffsetLine = line;
315 }
316 }
317
318 // Remove replaced content by settings this variable
319 replacmentEnd = Math.max(
320 replacmentEnd,
321 Math.floor(repls[i].end + 1)
322 );
323
324 // Move to next replacment
325 i++;
326 nextReplacement =
327 i < repls.length
328 ? Math.floor(repls[i].start)
329 : MAX_SOURCE_POSITION;
330
331 // Skip over when it has been replaced
332 const offset = chunk.length - endPos + replacmentEnd - chunkPos;
333 if (offset > 0) {
334 // Skip over whole chunk
335 if (replacmentEnd >= endPos) {
336 let line = generatedLine + generatedLineOffset;
337 if (chunk.endsWith("\n")) {
338 generatedLineOffset--;
339 if (generatedColumnOffsetLine === line) {
340 // undo exiting corrections form the current line
341 generatedColumnOffset += generatedColumn;
342 }
343 } else if (generatedColumnOffsetLine === line) {
344 generatedColumnOffset -= chunk.length - chunkPos;
345 } else {
346 generatedColumnOffset = chunkPos - chunk.length;
347 generatedColumnOffsetLine = line;
348 }
349 pos = endPos;
350 return;
351 }
352
353 // Partially skip over chunk
354 const line = generatedLine + generatedLineOffset;
355 if (
356 checkOriginalContent(
357 sourceIndex,
358 originalLine,
359 originalColumn,
360 chunk.slice(chunkPos, chunkPos + offset)
361 )
362 ) {
363 originalColumn += offset;
364 }
365 chunkPos += offset;
366 pos += offset;
367 if (generatedColumnOffsetLine === line) {
368 generatedColumnOffset -= offset;
369 } else {
370 generatedColumnOffset = -offset;
371 generatedColumnOffsetLine = line;
372 }
373 generatedColumn += offset;
374 }
375 } while (nextReplacement < endPos);
376 }
377
378 // Emit remaining chunk
379 if (chunkPos < chunk.length) {
380 const chunkSlice = chunkPos === 0 ? chunk : chunk.slice(chunkPos);
381 const line = generatedLine + generatedLineOffset;
382 onChunk(
383 chunkSlice,
384 line,
385 generatedColumn +
386 (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
387 sourceIndex,
388 originalLine,
389 originalColumn,
390 nameIndex < 0 ? -1 : nameIndexMapping[nameIndex]
391 );
392 }
393 pos = endPos;
394 },
395 (sourceIndex, source, sourceContent) => {
396 while (sourceContents.length < sourceIndex)
397 sourceContents.push(undefined);
398 sourceContents[sourceIndex] = sourceContent;
399 onSource(sourceIndex, source, sourceContent);
400 },
401 (nameIndex, name) => {
402 let globalIndex = nameMapping.get(name);
403 if (globalIndex === undefined) {
404 globalIndex = nameMapping.size;
405 nameMapping.set(name, globalIndex);
406 onName(globalIndex, name);
407 }
408 nameIndexMapping[nameIndex] = globalIndex;
409 }
410 );
411
412 // Handle remaining replacements
413 let remainer = "";
414 for (; i < repls.length; i++) {
415 remainer += repls[i].content;
416 }
417
418 // Insert remaining replacements content splitted into chunks by lines
419 let line = generatedLine + generatedLineOffset;
420 const regexp = /[^\n]+\n?|\n/g;
421 let match = regexp.exec(remainer);
422 while (match !== null) {
423 const contentLine = match[0];
424 onChunk(
425 contentLine,
426 line,
427 generatedColumn +
428 (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
429 -1,
430 -1,
431 -1,
432 -1
433 );
434
435 match = regexp.exec(remainer);
436 if (match === null && !contentLine.endsWith("\n")) {
437 if (generatedColumnOffsetLine === line) {
438 generatedColumnOffset += contentLine.length;
439 } else {
440 generatedColumnOffset = contentLine.length;
441 generatedColumnOffsetLine = line;
442 }
443 } else {
444 generatedLineOffset++;
445 line++;
446 generatedColumnOffset = -generatedColumn;
447 generatedColumnOffsetLine = line;
448 }
449 }
450
451 return {
452 generatedLine: line,
453 generatedColumn:
454 generatedColumn +
455 (line === generatedColumnOffsetLine ? generatedColumnOffset : 0)
456 };
457 }
458
459 updateHash(hash) {
460 this._sortReplacements();
461 hash.update("ReplaceSource");
462 this._source.updateHash(hash);
463 hash.update(this._name || "");
464 for (const repl of this._replacements) {
465 hash.update(`${repl.start}${repl.end}${repl.content}${repl.name}`);
466 }
467 }
468}
469
470module.exports = ReplaceSource;