source-node.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. /* -*- Mode: js; js-indent-level: 2; -*- */
  2. /*
  3. * Copyright 2011 Mozilla Foundation and contributors
  4. * Licensed under the New BSD license. See LICENSE or:
  5. * http://opensource.org/licenses/BSD-3-Clause
  6. */
  7. var SourceMapGenerator = require('./source-map-generator').SourceMapGenerator;
  8. var util = require('./util');
  9. // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other
  10. // operating systems these days (capturing the result).
  11. var REGEX_NEWLINE = /(\r?\n)/;
  12. // Newline character code for charCodeAt() comparisons
  13. var NEWLINE_CODE = 10;
  14. // Private symbol for identifying `SourceNode`s when multiple versions of
  15. // the source-map library are loaded. This MUST NOT CHANGE across
  16. // versions!
  17. var isSourceNode = "$$$isSourceNode$$$";
  18. /**
  19. * SourceNodes provide a way to abstract over interpolating/concatenating
  20. * snippets of generated JavaScript source code while maintaining the line and
  21. * column information associated with the original source code.
  22. *
  23. * @param aLine The original line number.
  24. * @param aColumn The original column number.
  25. * @param aSource The original source's filename.
  26. * @param aChunks Optional. An array of strings which are snippets of
  27. * generated JS, or other SourceNodes.
  28. * @param aName The original identifier.
  29. */
  30. function SourceNode(aLine, aColumn, aSource, aChunks, aName) {
  31. this.children = [];
  32. this.sourceContents = {};
  33. this.line = aLine == null ? null : aLine;
  34. this.column = aColumn == null ? null : aColumn;
  35. this.source = aSource == null ? null : aSource;
  36. this.name = aName == null ? null : aName;
  37. this[isSourceNode] = true;
  38. if (aChunks != null) this.add(aChunks);
  39. }
  40. /**
  41. * Creates a SourceNode from generated code and a SourceMapConsumer.
  42. *
  43. * @param aGeneratedCode The generated code
  44. * @param aSourceMapConsumer The SourceMap for the generated code
  45. * @param aRelativePath Optional. The path that relative sources in the
  46. * SourceMapConsumer should be relative to.
  47. */
  48. SourceNode.fromStringWithSourceMap =
  49. function SourceNode_fromStringWithSourceMap(aGeneratedCode, aSourceMapConsumer, aRelativePath) {
  50. // The SourceNode we want to fill with the generated code
  51. // and the SourceMap
  52. var node = new SourceNode();
  53. // All even indices of this array are one line of the generated code,
  54. // while all odd indices are the newlines between two adjacent lines
  55. // (since `REGEX_NEWLINE` captures its match).
  56. // Processed fragments are accessed by calling `shiftNextLine`.
  57. var remainingLines = aGeneratedCode.split(REGEX_NEWLINE);
  58. var remainingLinesIndex = 0;
  59. var shiftNextLine = function() {
  60. var lineContents = getNextLine();
  61. // The last line of a file might not have a newline.
  62. var newLine = getNextLine() || "";
  63. return lineContents + newLine;
  64. function getNextLine() {
  65. return remainingLinesIndex < remainingLines.length ?
  66. remainingLines[remainingLinesIndex++] : undefined;
  67. }
  68. };
  69. // We need to remember the position of "remainingLines"
  70. var lastGeneratedLine = 1, lastGeneratedColumn = 0;
  71. // The generate SourceNodes we need a code range.
  72. // To extract it current and last mapping is used.
  73. // Here we store the last mapping.
  74. var lastMapping = null;
  75. aSourceMapConsumer.eachMapping(function (mapping) {
  76. if (lastMapping !== null) {
  77. // We add the code from "lastMapping" to "mapping":
  78. // First check if there is a new line in between.
  79. if (lastGeneratedLine < mapping.generatedLine) {
  80. // Associate first line with "lastMapping"
  81. addMappingWithCode(lastMapping, shiftNextLine());
  82. lastGeneratedLine++;
  83. lastGeneratedColumn = 0;
  84. // The remaining code is added without mapping
  85. } else {
  86. // There is no new line in between.
  87. // Associate the code between "lastGeneratedColumn" and
  88. // "mapping.generatedColumn" with "lastMapping"
  89. var nextLine = remainingLines[remainingLinesIndex] || '';
  90. var code = nextLine.substr(0, mapping.generatedColumn -
  91. lastGeneratedColumn);
  92. remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn -
  93. lastGeneratedColumn);
  94. lastGeneratedColumn = mapping.generatedColumn;
  95. addMappingWithCode(lastMapping, code);
  96. // No more remaining code, continue
  97. lastMapping = mapping;
  98. return;
  99. }
  100. }
  101. // We add the generated code until the first mapping
  102. // to the SourceNode without any mapping.
  103. // Each line is added as separate string.
  104. while (lastGeneratedLine < mapping.generatedLine) {
  105. if (remainingLinesIndex >= remainingLines.length) {
  106. // The generated code is exhausted. Skip the rest instead of adding
  107. // empty lines one by one: a mapping may point far past the end of
  108. // the code (e.g. an indexed map section with a huge offset.line).
  109. lastGeneratedLine = mapping.generatedLine;
  110. break;
  111. }
  112. node.add(shiftNextLine());
  113. lastGeneratedLine++;
  114. }
  115. if (lastGeneratedColumn < mapping.generatedColumn) {
  116. var nextLine = remainingLines[remainingLinesIndex] || '';
  117. node.add(nextLine.substr(0, mapping.generatedColumn));
  118. remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn);
  119. lastGeneratedColumn = mapping.generatedColumn;
  120. }
  121. lastMapping = mapping;
  122. }, this);
  123. // We have processed all mappings.
  124. if (remainingLinesIndex < remainingLines.length) {
  125. if (lastMapping) {
  126. // Associate the remaining code in the current line with "lastMapping"
  127. addMappingWithCode(lastMapping, shiftNextLine());
  128. }
  129. // and add the remaining lines without any mapping
  130. node.add(remainingLines.splice(remainingLinesIndex).join(""));
  131. }
  132. // Copy sourcesContent into SourceNode
  133. aSourceMapConsumer.sources.forEach(function (sourceFile) {
  134. var content = aSourceMapConsumer.sourceContentFor(sourceFile);
  135. if (content != null) {
  136. if (aRelativePath != null) {
  137. sourceFile = util.join(aRelativePath, sourceFile);
  138. }
  139. node.setSourceContent(sourceFile, content);
  140. }
  141. });
  142. return node;
  143. function addMappingWithCode(mapping, code) {
  144. if (mapping === null || mapping.source === undefined) {
  145. node.add(code);
  146. } else {
  147. var source = aRelativePath
  148. ? util.join(aRelativePath, mapping.source)
  149. : mapping.source;
  150. node.add(new SourceNode(mapping.originalLine,
  151. mapping.originalColumn,
  152. source,
  153. code,
  154. mapping.name));
  155. }
  156. }
  157. };
  158. /**
  159. * Add a chunk of generated JS to this source node.
  160. *
  161. * @param aChunk A string snippet of generated JS code, another instance of
  162. * SourceNode, or an array where each member is one of those things.
  163. */
  164. SourceNode.prototype.add = function SourceNode_add(aChunk) {
  165. if (Array.isArray(aChunk)) {
  166. aChunk.forEach(function (chunk) {
  167. this.add(chunk);
  168. }, this);
  169. }
  170. else if (aChunk[isSourceNode] || typeof aChunk === "string") {
  171. if (aChunk) {
  172. this.children.push(aChunk);
  173. }
  174. }
  175. else {
  176. throw new TypeError(
  177. "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
  178. );
  179. }
  180. return this;
  181. };
  182. /**
  183. * Add a chunk of generated JS to the beginning of this source node.
  184. *
  185. * @param aChunk A string snippet of generated JS code, another instance of
  186. * SourceNode, or an array where each member is one of those things.
  187. */
  188. SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) {
  189. if (Array.isArray(aChunk)) {
  190. for (var i = aChunk.length-1; i >= 0; i--) {
  191. this.prepend(aChunk[i]);
  192. }
  193. }
  194. else if (aChunk[isSourceNode] || typeof aChunk === "string") {
  195. this.children.unshift(aChunk);
  196. }
  197. else {
  198. throw new TypeError(
  199. "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
  200. );
  201. }
  202. return this;
  203. };
  204. /**
  205. * Walk over the tree of JS snippets in this node and its children. The
  206. * walking function is called once for each snippet of JS and is passed that
  207. * snippet and the its original associated source's line/column location.
  208. *
  209. * @param aFn The traversal function.
  210. */
  211. SourceNode.prototype.walk = function SourceNode_walk(aFn) {
  212. var chunk;
  213. for (var i = 0, len = this.children.length; i < len; i++) {
  214. chunk = this.children[i];
  215. if (chunk[isSourceNode]) {
  216. chunk.walk(aFn);
  217. }
  218. else {
  219. if (chunk !== '') {
  220. aFn(chunk, { source: this.source,
  221. line: this.line,
  222. column: this.column,
  223. name: this.name });
  224. }
  225. }
  226. }
  227. };
  228. /**
  229. * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
  230. * each of `this.children`.
  231. *
  232. * @param aSep The separator.
  233. */
  234. SourceNode.prototype.join = function SourceNode_join(aSep) {
  235. var newChildren;
  236. var i;
  237. var len = this.children.length;
  238. if (len > 0) {
  239. newChildren = [];
  240. for (i = 0; i < len-1; i++) {
  241. newChildren.push(this.children[i]);
  242. newChildren.push(aSep);
  243. }
  244. newChildren.push(this.children[i]);
  245. this.children = newChildren;
  246. }
  247. return this;
  248. };
  249. /**
  250. * Call String.prototype.replace on the very right-most source snippet. Useful
  251. * for trimming whitespace from the end of a source node, etc.
  252. *
  253. * @param aPattern The pattern to replace.
  254. * @param aReplacement The thing to replace the pattern with.
  255. */
  256. SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) {
  257. var lastChild = this.children[this.children.length - 1];
  258. if (lastChild[isSourceNode]) {
  259. lastChild.replaceRight(aPattern, aReplacement);
  260. }
  261. else if (typeof lastChild === 'string') {
  262. this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
  263. }
  264. else {
  265. this.children.push(''.replace(aPattern, aReplacement));
  266. }
  267. return this;
  268. };
  269. /**
  270. * Set the source content for a source file. This will be added to the SourceMapGenerator
  271. * in the sourcesContent field.
  272. *
  273. * @param aSourceFile The filename of the source file
  274. * @param aSourceContent The content of the source file
  275. */
  276. SourceNode.prototype.setSourceContent =
  277. function SourceNode_setSourceContent(aSourceFile, aSourceContent) {
  278. this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
  279. };
  280. /**
  281. * Walk over the tree of SourceNodes. The walking function is called for each
  282. * source file content and is passed the filename and source content.
  283. *
  284. * @param aFn The traversal function.
  285. */
  286. SourceNode.prototype.walkSourceContents =
  287. function SourceNode_walkSourceContents(aFn) {
  288. for (var i = 0, len = this.children.length; i < len; i++) {
  289. if (this.children[i][isSourceNode]) {
  290. this.children[i].walkSourceContents(aFn);
  291. }
  292. }
  293. var sources = Object.keys(this.sourceContents);
  294. for (var i = 0, len = sources.length; i < len; i++) {
  295. aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
  296. }
  297. };
  298. /**
  299. * Return the string representation of this source node. Walks over the tree
  300. * and concatenates all the various snippets together to one string.
  301. */
  302. SourceNode.prototype.toString = function SourceNode_toString() {
  303. var str = "";
  304. this.walk(function (chunk) {
  305. str += chunk;
  306. });
  307. return str;
  308. };
  309. /**
  310. * Returns the string representation of this source node along with a source
  311. * map.
  312. */
  313. SourceNode.prototype.toStringWithSourceMap = function SourceNode_toStringWithSourceMap(aArgs) {
  314. var generated = {
  315. code: "",
  316. line: 1,
  317. column: 0
  318. };
  319. var map = new SourceMapGenerator(aArgs);
  320. var sourceMappingActive = false;
  321. var lastOriginalSource = null;
  322. var lastOriginalLine = null;
  323. var lastOriginalColumn = null;
  324. var lastOriginalName = null;
  325. this.walk(function (chunk, original) {
  326. generated.code += chunk;
  327. if (original.source !== null
  328. && original.line !== null
  329. && original.column !== null) {
  330. if(lastOriginalSource !== original.source
  331. || lastOriginalLine !== original.line
  332. || lastOriginalColumn !== original.column
  333. || lastOriginalName !== original.name) {
  334. map.addMapping({
  335. source: original.source,
  336. original: {
  337. line: original.line,
  338. column: original.column
  339. },
  340. generated: {
  341. line: generated.line,
  342. column: generated.column
  343. },
  344. name: original.name
  345. });
  346. }
  347. lastOriginalSource = original.source;
  348. lastOriginalLine = original.line;
  349. lastOriginalColumn = original.column;
  350. lastOriginalName = original.name;
  351. sourceMappingActive = true;
  352. } else if (sourceMappingActive) {
  353. map.addMapping({
  354. generated: {
  355. line: generated.line,
  356. column: generated.column
  357. }
  358. });
  359. lastOriginalSource = null;
  360. sourceMappingActive = false;
  361. }
  362. for (var idx = 0, length = chunk.length; idx < length; idx++) {
  363. if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
  364. generated.line++;
  365. generated.column = 0;
  366. // Mappings end at eol
  367. if (idx + 1 === length) {
  368. lastOriginalSource = null;
  369. sourceMappingActive = false;
  370. } else if (sourceMappingActive) {
  371. map.addMapping({
  372. source: original.source,
  373. original: {
  374. line: original.line,
  375. column: original.column
  376. },
  377. generated: {
  378. line: generated.line,
  379. column: generated.column
  380. },
  381. name: original.name
  382. });
  383. }
  384. } else {
  385. generated.column++;
  386. }
  387. }
  388. });
  389. this.walkSourceContents(function (sourceFile, sourceContent) {
  390. map.setSourceContent(sourceFile, sourceContent);
  391. });
  392. return { code: generated.code, map: map };
  393. };
  394. exports.SourceNode = SourceNode;