UNPKG

12.5 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 + 1, 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, 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);
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 } else if (generatedColumnOffsetLine === line) {
199 generatedColumnOffset -= chunk.length;
200 } else {
201 generatedColumnOffset = -chunk.length;
202 generatedColumnOffsetLine = line;
203 }
204 pos = endPos;
205 return;
206 }
207
208 // Partially skip over chunk
209 chunkPos = replacmentEnd - pos;
210 if (
211 checkOriginalContent(
212 sourceIndex,
213 originalLine,
214 originalColumn,
215 chunk.slice(0, chunkPos)
216 )
217 ) {
218 originalColumn += chunkPos;
219 }
220 pos += chunkPos;
221 const line = generatedLine + generatedLineOffset;
222 if (generatedColumnOffsetLine === line) {
223 generatedColumnOffset -= chunkPos;
224 } else {
225 generatedColumnOffset = -chunkPos;
226 generatedColumnOffsetLine = line;
227 }
228 generatedColumn += chunkPos;
229 }
230
231 // Is a replacement in the chunk?
232 if (nextReplacement < endPos) {
233 do {
234 let line = generatedLine + generatedLineOffset;
235 if (nextReplacement > pos) {
236 // Emit chunk until replacement
237 const offset = nextReplacement - pos;
238 const chunkSlice = chunk.slice(chunkPos, chunkPos + offset);
239 onChunk(
240 chunkSlice,
241 line,
242 generatedColumn +
243 (line === generatedColumnOffsetLine
244 ? generatedColumnOffset
245 : 0),
246 sourceIndex,
247 originalLine,
248 originalColumn,
249 nameIndex < 0 ? -1 : nameIndexMapping[nameIndex]
250 );
251 generatedColumn += offset;
252 chunkPos += offset;
253 pos = nextReplacement;
254 if (
255 checkOriginalContent(
256 sourceIndex,
257 originalLine,
258 originalColumn,
259 chunkSlice
260 )
261 ) {
262 originalColumn += chunkSlice.length;
263 }
264 }
265
266 // Insert replacement content splitted into chunks by lines
267 const regexp = /[^\n]+\n?|\n/g;
268 const { content, name } = repls[i];
269 let match = regexp.exec(content);
270 let replacementNameIndex = nameIndex;
271 if (name) {
272 let globalIndex = nameMapping.get(name);
273 if (globalIndex === undefined) {
274 globalIndex = nameMapping.size;
275 nameMapping.set(name, globalIndex);
276 onName(globalIndex, name);
277 }
278 replacementNameIndex = globalIndex;
279 }
280 while (match !== null) {
281 const contentLine = match[0];
282 onChunk(
283 contentLine,
284 line,
285 generatedColumn +
286 (line === generatedColumnOffsetLine
287 ? generatedColumnOffset
288 : 0),
289 sourceIndex,
290 originalLine,
291 originalColumn,
292 replacementNameIndex
293 );
294
295 // Only the first chunk has name assigned
296 replacementNameIndex = -1;
297
298 match = regexp.exec(content);
299 if (match === null && !contentLine.endsWith("\n")) {
300 if (generatedColumnOffsetLine === line) {
301 generatedColumnOffset += contentLine.length;
302 } else {
303 generatedColumnOffset = contentLine.length;
304 generatedColumnOffsetLine = line;
305 }
306 } else {
307 generatedLineOffset++;
308 line++;
309 generatedColumnOffset = -generatedColumn;
310 generatedColumnOffsetLine = line;
311 }
312 }
313
314 // Remove replaced content by settings this variable
315 replacmentEnd = Math.max(replacmentEnd, Math.floor(repls[i].end));
316
317 // Move to next replacment
318 i++;
319 nextReplacement =
320 i < repls.length
321 ? Math.floor(repls[i].start)
322 : MAX_SOURCE_POSITION;
323
324 // Skip over when it has been replaced
325 const offset = chunk.length - endPos + replacmentEnd - chunkPos;
326 if (offset > 0) {
327 // Skip over whole chunk
328 if (replacmentEnd >= endPos) {
329 let line = generatedLine + generatedLineOffset;
330 if (chunk.endsWith("\n")) {
331 generatedLineOffset--;
332 if (generatedColumnOffsetLine === line) {
333 generatedColumnOffset += generatedColumn;
334 } else {
335 generatedColumnOffset = generatedColumn;
336 generatedColumnOffsetLine = line;
337 }
338 } else if (generatedColumnOffsetLine === line) {
339 generatedColumnOffset -= chunk.length - chunkPos;
340 } else {
341 generatedColumnOffset = chunkPos - chunk.length;
342 generatedColumnOffsetLine = line;
343 }
344 pos = endPos;
345 return;
346 }
347
348 // Partially skip over chunk
349 const line = generatedLine + generatedLineOffset;
350 if (
351 checkOriginalContent(
352 sourceIndex,
353 originalLine,
354 originalColumn,
355 chunk.slice(chunkPos, chunkPos + offset)
356 )
357 ) {
358 originalColumn += offset;
359 }
360 chunkPos += offset;
361 pos += offset;
362 if (generatedColumnOffsetLine === line) {
363 generatedColumnOffset -= offset;
364 } else {
365 generatedColumnOffset = -offset;
366 generatedColumnOffsetLine = line;
367 }
368 generatedColumn += offset;
369 }
370 } while (nextReplacement < endPos);
371 }
372
373 // Emit remaining chunk
374 if (chunkPos < chunk.length) {
375 const chunkSlice = chunkPos === 0 ? chunk : chunk.slice(chunkPos);
376 const line = generatedLine + generatedLineOffset;
377 onChunk(
378 chunkSlice,
379 line,
380 generatedColumn +
381 (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
382 sourceIndex,
383 originalLine,
384 originalColumn,
385 nameIndex < 0 ? -1 : nameIndexMapping[nameIndex]
386 );
387 }
388 pos = endPos;
389 },
390 (sourceIndex, source, sourceContent) => {
391 while (sourceContents.length < sourceIndex)
392 sourceContents.push(undefined);
393 sourceContents[sourceIndex] = sourceContent;
394 onSource(sourceIndex, source, sourceContent);
395 },
396 (nameIndex, name) => {
397 let globalIndex = nameMapping.get(name);
398 if (globalIndex === undefined) {
399 globalIndex = nameMapping.size;
400 nameMapping.set(name, globalIndex);
401 onName(globalIndex, name);
402 }
403 nameIndexMapping[nameIndex] = globalIndex;
404 }
405 );
406
407 // Handle remaining replacements
408 let remainer = "";
409 for (; i < repls.length; i++) {
410 remainer += repls[i].content;
411 }
412
413 // Insert remaining replacements content splitted into chunks by lines
414 let line = generatedLine + generatedLineOffset;
415 const regexp = /[^\n]+\n?|\n/g;
416 let match = regexp.exec(remainer);
417 while (match !== null) {
418 const contentLine = match[0];
419 onChunk(
420 contentLine,
421 line,
422 generatedColumn +
423 (line === generatedColumnOffsetLine ? generatedColumnOffset : 0),
424 -1,
425 -1,
426 -1,
427 -1
428 );
429
430 match = regexp.exec(remainer);
431 if (match === null && !contentLine.endsWith("\n")) {
432 if (generatedColumnOffsetLine === line) {
433 generatedColumnOffset += contentLine.length;
434 } else {
435 generatedColumnOffset = contentLine.length;
436 generatedColumnOffsetLine = line;
437 }
438 } else {
439 generatedLineOffset++;
440 line++;
441 generatedColumnOffset = -generatedColumn;
442 generatedColumnOffsetLine = line;
443 }
444 }
445
446 return {
447 generatedLine: line,
448 generatedColumn:
449 generatedColumn +
450 (line === generatedColumnOffsetLine ? generatedColumnOffset : 0)
451 };
452 }
453
454 updateHash(hash) {
455 this._sortReplacements();
456 hash.update("ReplaceSource");
457 this._source.updateHash(hash);
458 hash.update(this._name || "");
459 for (const repl of this._replacements) {
460 hash.update(`${repl.start}`);
461 hash.update(`${repl.end}`);
462 hash.update(`${repl.content}`);
463 hash.update(`${repl.insertIndex}`);
464 hash.update(`${repl.name}`);
465 }
466 }
467}
468
469module.exports = ReplaceSource;