source-node.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  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. node.add(shiftNextLine());
  106. lastGeneratedLine++;
  107. }
  108. if (lastGeneratedColumn < mapping.generatedColumn) {
  109. var nextLine = remainingLines[remainingLinesIndex];
  110. node.add(nextLine.substr(0, mapping.generatedColumn));
  111. remainingLines[remainingLinesIndex] = nextLine.substr(mapping.generatedColumn);
  112. lastGeneratedColumn = mapping.generatedColumn;
  113. }
  114. lastMapping = mapping;
  115. }, this);
  116. // We have processed all mappings.
  117. if (remainingLinesIndex < remainingLines.length) {
  118. if (lastMapping) {
  119. // Associate the remaining code in the current line with "lastMapping"
  120. addMappingWithCode(lastMapping, shiftNextLine());
  121. }
  122. // and add the remaining lines without any mapping
  123. node.add(remainingLines.splice(remainingLinesIndex).join(""));
  124. }
  125. // Copy sourcesContent into SourceNode
  126. aSourceMapConsumer.sources.forEach(function (sourceFile) {
  127. var content = aSourceMapConsumer.sourceContentFor(sourceFile);
  128. if (content != null) {
  129. if (aRelativePath != null) {
  130. sourceFile = util.join(aRelativePath, sourceFile);
  131. }
  132. node.setSourceContent(sourceFile, content);
  133. }
  134. });
  135. return node;
  136. function addMappingWithCode(mapping, code) {
  137. if (mapping === null || mapping.source === undefined) {
  138. node.add(code);
  139. } else {
  140. var source = aRelativePath
  141. ? util.join(aRelativePath, mapping.source)
  142. : mapping.source;
  143. node.add(new SourceNode(mapping.originalLine,
  144. mapping.originalColumn,
  145. source,
  146. code,
  147. mapping.name));
  148. }
  149. }
  150. };
  151. /**
  152. * Add a chunk of generated JS to this source node.
  153. *
  154. * @param aChunk A string snippet of generated JS code, another instance of
  155. * SourceNode, or an array where each member is one of those things.
  156. */
  157. SourceNode.prototype.add = function SourceNode_add(aChunk) {
  158. if (Array.isArray(aChunk)) {
  159. aChunk.forEach(function (chunk) {
  160. this.add(chunk);
  161. }, this);
  162. }
  163. else if (aChunk[isSourceNode] || typeof aChunk === "string") {
  164. if (aChunk) {
  165. this.children.push(aChunk);
  166. }
  167. }
  168. else {
  169. throw new TypeError(
  170. "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
  171. );
  172. }
  173. return this;
  174. };
  175. /**
  176. * Add a chunk of generated JS to the beginning of this source node.
  177. *
  178. * @param aChunk A string snippet of generated JS code, another instance of
  179. * SourceNode, or an array where each member is one of those things.
  180. */
  181. SourceNode.prototype.prepend = function SourceNode_prepend(aChunk) {
  182. if (Array.isArray(aChunk)) {
  183. for (var i = aChunk.length-1; i >= 0; i--) {
  184. this.prepend(aChunk[i]);
  185. }
  186. }
  187. else if (aChunk[isSourceNode] || typeof aChunk === "string") {
  188. this.children.unshift(aChunk);
  189. }
  190. else {
  191. throw new TypeError(
  192. "Expected a SourceNode, string, or an array of SourceNodes and strings. Got " + aChunk
  193. );
  194. }
  195. return this;
  196. };
  197. /**
  198. * Walk over the tree of JS snippets in this node and its children. The
  199. * walking function is called once for each snippet of JS and is passed that
  200. * snippet and the its original associated source's line/column location.
  201. *
  202. * @param aFn The traversal function.
  203. */
  204. SourceNode.prototype.walk = function SourceNode_walk(aFn) {
  205. var chunk;
  206. for (var i = 0, len = this.children.length; i < len; i++) {
  207. chunk = this.children[i];
  208. if (chunk[isSourceNode]) {
  209. chunk.walk(aFn);
  210. }
  211. else {
  212. if (chunk !== '') {
  213. aFn(chunk, { source: this.source,
  214. line: this.line,
  215. column: this.column,
  216. name: this.name });
  217. }
  218. }
  219. }
  220. };
  221. /**
  222. * Like `String.prototype.join` except for SourceNodes. Inserts `aStr` between
  223. * each of `this.children`.
  224. *
  225. * @param aSep The separator.
  226. */
  227. SourceNode.prototype.join = function SourceNode_join(aSep) {
  228. var newChildren;
  229. var i;
  230. var len = this.children.length;
  231. if (len > 0) {
  232. newChildren = [];
  233. for (i = 0; i < len-1; i++) {
  234. newChildren.push(this.children[i]);
  235. newChildren.push(aSep);
  236. }
  237. newChildren.push(this.children[i]);
  238. this.children = newChildren;
  239. }
  240. return this;
  241. };
  242. /**
  243. * Call String.prototype.replace on the very right-most source snippet. Useful
  244. * for trimming whitespace from the end of a source node, etc.
  245. *
  246. * @param aPattern The pattern to replace.
  247. * @param aReplacement The thing to replace the pattern with.
  248. */
  249. SourceNode.prototype.replaceRight = function SourceNode_replaceRight(aPattern, aReplacement) {
  250. var lastChild = this.children[this.children.length - 1];
  251. if (lastChild[isSourceNode]) {
  252. lastChild.replaceRight(aPattern, aReplacement);
  253. }
  254. else if (typeof lastChild === 'string') {
  255. this.children[this.children.length - 1] = lastChild.replace(aPattern, aReplacement);
  256. }
  257. else {
  258. this.children.push(''.replace(aPattern, aReplacement));
  259. }
  260. return this;
  261. };
  262. /**
  263. * Set the source content for a source file. This will be added to the SourceMapGenerator
  264. * in the sourcesContent field.
  265. *
  266. * @param aSourceFile The filename of the source file
  267. * @param aSourceContent The content of the source file
  268. */
  269. SourceNode.prototype.setSourceContent =
  270. function SourceNode_setSourceContent(aSourceFile, aSourceContent) {
  271. this.sourceContents[util.toSetString(aSourceFile)] = aSourceContent;
  272. };
  273. /**
  274. * Walk over the tree of SourceNodes. The walking function is called for each
  275. * source file content and is passed the filename and source content.
  276. *
  277. * @param aFn The traversal function.
  278. */
  279. SourceNode.prototype.walkSourceContents =
  280. function SourceNode_walkSourceContents(aFn) {
  281. for (var i = 0, len = this.children.length; i < len; i++) {
  282. if (this.children[i][isSourceNode]) {
  283. this.children[i].walkSourceContents(aFn);
  284. }
  285. }
  286. var sources = Object.keys(this.sourceContents);
  287. for (var i = 0, len = sources.length; i < len; i++) {
  288. aFn(util.fromSetString(sources[i]), this.sourceContents[sources[i]]);
  289. }
  290. };
  291. /**
  292. * Return the string representation of this source node. Walks over the tree
  293. * and concatenates all the various snippets together to one string.
  294. */
  295. SourceNode.prototype.toString = function SourceNode_toString() {
  296. var str = "";
  297. this.walk(function (chunk) {
  298. str += chunk;
  299. });
  300. return str;
  301. };
  302. /**
  303. * Returns the string representation of this source node along with a source
  304. * map.
  305. */
  306. SourceNode.prototype.toStringWithSourceMap = function SourceNode_toStringWithSourceMap(aArgs) {
  307. var generated = {
  308. code: "",
  309. line: 1,
  310. column: 0
  311. };
  312. var map = new SourceMapGenerator(aArgs);
  313. var sourceMappingActive = false;
  314. var lastOriginalSource = null;
  315. var lastOriginalLine = null;
  316. var lastOriginalColumn = null;
  317. var lastOriginalName = null;
  318. this.walk(function (chunk, original) {
  319. generated.code += chunk;
  320. if (original.source !== null
  321. && original.line !== null
  322. && original.column !== null) {
  323. if(lastOriginalSource !== original.source
  324. || lastOriginalLine !== original.line
  325. || lastOriginalColumn !== original.column
  326. || lastOriginalName !== original.name) {
  327. map.addMapping({
  328. source: original.source,
  329. original: {
  330. line: original.line,
  331. column: original.column
  332. },
  333. generated: {
  334. line: generated.line,
  335. column: generated.column
  336. },
  337. name: original.name
  338. });
  339. }
  340. lastOriginalSource = original.source;
  341. lastOriginalLine = original.line;
  342. lastOriginalColumn = original.column;
  343. lastOriginalName = original.name;
  344. sourceMappingActive = true;
  345. } else if (sourceMappingActive) {
  346. map.addMapping({
  347. generated: {
  348. line: generated.line,
  349. column: generated.column
  350. }
  351. });
  352. lastOriginalSource = null;
  353. sourceMappingActive = false;
  354. }
  355. for (var idx = 0, length = chunk.length; idx < length; idx++) {
  356. if (chunk.charCodeAt(idx) === NEWLINE_CODE) {
  357. generated.line++;
  358. generated.column = 0;
  359. // Mappings end at eol
  360. if (idx + 1 === length) {
  361. lastOriginalSource = null;
  362. sourceMappingActive = false;
  363. } else if (sourceMappingActive) {
  364. map.addMapping({
  365. source: original.source,
  366. original: {
  367. line: original.line,
  368. column: original.column
  369. },
  370. generated: {
  371. line: generated.line,
  372. column: generated.column
  373. },
  374. name: original.name
  375. });
  376. }
  377. } else {
  378. generated.column++;
  379. }
  380. }
  381. });
  382. this.walkSourceContents(function (sourceFile, sourceContent) {
  383. map.setSourceContent(sourceFile, sourceContent);
  384. });
  385. return { code: generated.code, map: map };
  386. };
  387. exports.SourceNode = SourceNode;