source-node.js 13 KB

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