ReplaceSource.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. var Source = require("./Source");
  7. var SourceNode = require("source-map").SourceNode;
  8. class Replacement {
  9. constructor(start, end, content, insertIndex, name) {
  10. this.start = start;
  11. this.end = end;
  12. this.content = content;
  13. this.insertIndex = insertIndex;
  14. this.name = name;
  15. }
  16. }
  17. class ReplaceSource extends Source {
  18. constructor(source, name) {
  19. super();
  20. this._source = source;
  21. this._name = name;
  22. /** @type {Replacement[]} */
  23. this.replacements = [];
  24. }
  25. replace(start, end, newValue, name) {
  26. if(typeof newValue !== "string")
  27. throw new Error("insertion must be a string, but is a " + typeof newValue);
  28. this.replacements.push(new Replacement(start, end, newValue, this.replacements.length, name));
  29. }
  30. insert(pos, newValue, name) {
  31. if(typeof newValue !== "string")
  32. throw new Error("insertion must be a string, but is a " + typeof newValue + ": " + newValue);
  33. this.replacements.push(new Replacement(pos, pos - 1, newValue, this.replacements.length, name));
  34. }
  35. source(options) {
  36. return this._replaceString(this._source.source());
  37. }
  38. original() {
  39. return this._source;
  40. }
  41. _sortReplacements() {
  42. this.replacements.sort(function(a, b) {
  43. var diff = b.end - a.end;
  44. if(diff !== 0)
  45. return diff;
  46. diff = b.start - a.start;
  47. if(diff !== 0)
  48. return diff;
  49. return b.insertIndex - a.insertIndex;
  50. });
  51. }
  52. _replaceString(str) {
  53. if(typeof str !== "string")
  54. throw new Error("str must be a string, but is a " + typeof str + ": " + str);
  55. this._sortReplacements();
  56. var result = [str];
  57. this.replacements.forEach(function(repl) {
  58. var remSource = result.pop();
  59. var splitted1 = this._splitString(remSource, Math.floor(repl.end + 1));
  60. var splitted2 = this._splitString(splitted1[0], Math.floor(repl.start));
  61. result.push(splitted1[1], repl.content, splitted2[0]);
  62. }, this);
  63. // write out result array in reverse order
  64. let resultStr = "";
  65. for(let i = result.length - 1; i >= 0; --i) {
  66. resultStr += result[i];
  67. }
  68. return resultStr;
  69. }
  70. node(options) {
  71. var node = this._source.node(options);
  72. if(this.replacements.length === 0) {
  73. return node;
  74. }
  75. this._sortReplacements();
  76. var replace = new ReplacementEnumerator(this.replacements);
  77. var output = [];
  78. var position = 0;
  79. var sources = Object.create(null);
  80. var sourcesInLines = Object.create(null);
  81. // We build a new list of SourceNodes in "output"
  82. // from the original mapping data
  83. var result = new SourceNode();
  84. // We need to add source contents manually
  85. // because "walk" will not handle it
  86. node.walkSourceContents(function(sourceFile, sourceContent) {
  87. result.setSourceContent(sourceFile, sourceContent);
  88. sources["$" + sourceFile] = sourceContent;
  89. });
  90. var replaceInStringNode = this._replaceInStringNode.bind(this, output, replace, function getOriginalSource(mapping) {
  91. var key = "$" + mapping.source;
  92. var lines = sourcesInLines[key];
  93. if(!lines) {
  94. var source = sources[key];
  95. if(!source) return null;
  96. lines = source.split("\n").map(function(line) {
  97. return line + "\n";
  98. });
  99. sourcesInLines[key] = lines;
  100. }
  101. // line is 1-based
  102. if(mapping.line > lines.length) return null;
  103. var line = lines[mapping.line - 1];
  104. return line.substr(mapping.column);
  105. });
  106. node.walk(function(chunk, mapping) {
  107. position = replaceInStringNode(chunk, position, mapping);
  108. });
  109. // If any replacements occur after the end of the original file, then we append them
  110. // directly to the end of the output
  111. var remaining = replace.footer();
  112. if(remaining) {
  113. output.push(remaining);
  114. }
  115. result.add(output);
  116. return result;
  117. }
  118. listMap(options) {
  119. this._sortReplacements();
  120. var map = this._source.listMap(options);
  121. var currentIndex = 0;
  122. var replacements = this.replacements;
  123. var idxReplacement = replacements.length - 1;
  124. var removeChars = 0;
  125. map = map.mapGeneratedCode(function(str) {
  126. var newCurrentIndex = currentIndex + str.length;
  127. if(removeChars > str.length) {
  128. removeChars -= str.length;
  129. str = "";
  130. } else {
  131. if(removeChars > 0) {
  132. str = str.substr(removeChars);
  133. currentIndex += removeChars;
  134. removeChars = 0;
  135. }
  136. var finalStr = "";
  137. while(idxReplacement >= 0 && replacements[idxReplacement].start < newCurrentIndex) {
  138. var repl = replacements[idxReplacement];
  139. var start = Math.floor(repl.start);
  140. var end = Math.floor(repl.end + 1);
  141. var before = str.substr(0, Math.max(0, start - currentIndex));
  142. if(end <= newCurrentIndex) {
  143. var after = str.substr(Math.max(0, end - currentIndex));
  144. finalStr += before + repl.content;
  145. str = after;
  146. currentIndex = Math.max(currentIndex, end);
  147. } else {
  148. finalStr += before + repl.content;
  149. str = "";
  150. removeChars = end - newCurrentIndex;
  151. }
  152. idxReplacement--;
  153. }
  154. str = finalStr + str;
  155. }
  156. currentIndex = newCurrentIndex;
  157. return str;
  158. });
  159. var extraCode = "";
  160. while(idxReplacement >= 0) {
  161. extraCode += replacements[idxReplacement].content;
  162. idxReplacement--;
  163. }
  164. if(extraCode) {
  165. map.add(extraCode);
  166. }
  167. return map;
  168. }
  169. _splitString(str, position) {
  170. return position <= 0 ? ["", str] : [str.substr(0, position), str.substr(position)];
  171. }
  172. _replaceInStringNode(output, replace, getOriginalSource, node, position, mapping) {
  173. var original = undefined;
  174. do {
  175. var splitPosition = replace.position - position;
  176. // If multiple replaces occur in the same location then the splitPosition may be
  177. // before the current position for the subsequent splits. Ensure it is >= 0
  178. if(splitPosition < 0) {
  179. splitPosition = 0;
  180. }
  181. if(splitPosition >= node.length || replace.done) {
  182. if(replace.emit) {
  183. var nodeEnd = new SourceNode(
  184. mapping.line,
  185. mapping.column,
  186. mapping.source,
  187. node,
  188. mapping.name
  189. );
  190. output.push(nodeEnd);
  191. }
  192. return position + node.length;
  193. }
  194. var originalColumn = mapping.column;
  195. // Try to figure out if generated code matches original code of this segement
  196. // If this is the case we assume that it's allowed to move mapping.column
  197. // Because getOriginalSource can be expensive we only do it when neccessary
  198. var nodePart;
  199. if(splitPosition > 0) {
  200. nodePart = node.slice(0, splitPosition);
  201. if(original === undefined) {
  202. original = getOriginalSource(mapping);
  203. }
  204. if(original && original.length >= splitPosition && original.startsWith(nodePart)) {
  205. mapping.column += splitPosition;
  206. original = original.substr(splitPosition);
  207. }
  208. }
  209. var emit = replace.next();
  210. if(!emit) {
  211. // Stop emitting when we have found the beginning of the string to replace.
  212. // Emit the part of the string before splitPosition
  213. if(splitPosition > 0) {
  214. var nodeStart = new SourceNode(
  215. mapping.line,
  216. originalColumn,
  217. mapping.source,
  218. nodePart,
  219. mapping.name
  220. );
  221. output.push(nodeStart);
  222. }
  223. // Emit the replacement value
  224. if(replace.value) {
  225. output.push(new SourceNode(
  226. mapping.line,
  227. mapping.column,
  228. mapping.source,
  229. replace.value,
  230. mapping.name || replace.name
  231. ));
  232. }
  233. }
  234. // Recurse with remainder of the string as there may be multiple replaces within a single node
  235. node = node.substr(splitPosition);
  236. position += splitPosition;
  237. } while (true);
  238. }
  239. }
  240. class ReplacementEnumerator {
  241. /**
  242. * @param {Replacement[]} replacements list of replacements
  243. */
  244. constructor(replacements) {
  245. this.replacements = replacements || [];
  246. this.index = this.replacements.length;
  247. this.done = false;
  248. this.emit = false;
  249. // Set initial start position
  250. this.next();
  251. }
  252. next() {
  253. if(this.done)
  254. return true;
  255. if(this.emit) {
  256. // Start point found. stop emitting. set position to find end
  257. var repl = this.replacements[this.index];
  258. var end = Math.floor(repl.end + 1);
  259. this.position = end;
  260. this.value = repl.content;
  261. this.name = repl.name;
  262. } else {
  263. // End point found. start emitting. set position to find next start
  264. this.index--;
  265. if(this.index < 0) {
  266. this.done = true;
  267. } else {
  268. var nextRepl = this.replacements[this.index];
  269. var start = Math.floor(nextRepl.start);
  270. this.position = start;
  271. }
  272. }
  273. if(this.position < 0)
  274. this.position = 0;
  275. this.emit = !this.emit;
  276. return this.emit;
  277. }
  278. footer() {
  279. if(!this.done && !this.emit)
  280. this.next(); // If we finished _replaceInNode mid emit we advance to next entry
  281. if(this.done) {
  282. return [];
  283. } else {
  284. var resultStr = "";
  285. for(var i = this.index; i >= 0; i--) {
  286. var repl = this.replacements[i];
  287. // this doesn't need to handle repl.name, because in SourceMaps generated code
  288. // without pointer to original source can't have a name
  289. resultStr += repl.content;
  290. }
  291. return resultStr;
  292. }
  293. }
  294. }
  295. require("./SourceAndMapMixin")(ReplaceSource.prototype);
  296. module.exports = ReplaceSource;