ReplaceSource.js 12 KB

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