compiler.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. /*!
  2. * Stylus - Compiler
  3. * Copyright (c) Automattic <developer.wordpress.com>
  4. * MIT Licensed
  5. */
  6. /**
  7. * Module dependencies.
  8. */
  9. var Visitor = require('./')
  10. , utils = require('../utils')
  11. , fs = require('fs');
  12. /**
  13. * Initialize a new `Compiler` with the given `root` Node
  14. * and the following `options`.
  15. *
  16. * Options:
  17. *
  18. * - `compress` Compress the CSS output (default: false)
  19. *
  20. * @param {Node} root
  21. * @api public
  22. */
  23. var Compiler = module.exports = function Compiler(root, options) {
  24. options = options || {};
  25. this.compress = options.compress;
  26. this.firebug = options.firebug;
  27. this.linenos = options.linenos;
  28. this.spaces = options['indent spaces'] || 2;
  29. this.indents = 1;
  30. Visitor.call(this, root);
  31. this.stack = [];
  32. };
  33. /**
  34. * Inherit from `Visitor.prototype`.
  35. */
  36. Compiler.prototype.__proto__ = Visitor.prototype;
  37. /**
  38. * Compile to css, and return a string of CSS.
  39. *
  40. * @return {String}
  41. * @api private
  42. */
  43. Compiler.prototype.compile = function(){
  44. return this.visit(this.root);
  45. };
  46. /**
  47. * Output `str`
  48. *
  49. * @param {String} str
  50. * @param {Node} node
  51. * @return {String}
  52. * @api private
  53. */
  54. Compiler.prototype.out = function(str, node){
  55. return str;
  56. };
  57. /**
  58. * Return indentation string.
  59. *
  60. * @return {String}
  61. * @api private
  62. */
  63. Compiler.prototype.__defineGetter__('indent', function(){
  64. if (this.compress) return '';
  65. return new Array(this.indents).join(Array(this.spaces + 1).join(' '));
  66. });
  67. /**
  68. * Check if given `node` needs brackets.
  69. *
  70. * @param {Node} node
  71. * @return {Boolean}
  72. * @api private
  73. */
  74. Compiler.prototype.needBrackets = function(node){
  75. return 1 == this.indents
  76. || 'atrule' != node.nodeName
  77. || node.hasOnlyProperties;
  78. };
  79. /**
  80. * Visit Root.
  81. */
  82. Compiler.prototype.visitRoot = function(block){
  83. this.buf = '';
  84. for (var i = 0, len = block.nodes.length; i < len; ++i) {
  85. var node = block.nodes[i];
  86. if (this.linenos || this.firebug) this.debugInfo(node);
  87. var ret = this.visit(node);
  88. if (ret) this.buf += this.out(ret + '\n', node);
  89. }
  90. return this.buf;
  91. };
  92. /**
  93. * Visit Block.
  94. */
  95. Compiler.prototype.visitBlock = function(block){
  96. var node
  97. , separator = this.compress ? '' : '\n'
  98. , needBrackets
  99. , lastPropertyIndex;
  100. if (block.hasProperties && !block.lacksRenderedSelectors) {
  101. needBrackets = this.needBrackets(block.node);
  102. if (this.compress) {
  103. for (var i = block.nodes.length - 1; i >= 0; --i) {
  104. if (block.nodes[i].nodeName === 'property') {
  105. lastPropertyIndex = i;
  106. break;
  107. }
  108. }
  109. }
  110. if (needBrackets) {
  111. this.buf += this.out(this.compress ? '{' : ' {\n');
  112. ++this.indents;
  113. }
  114. for (var i = 0, len = block.nodes.length; i < len; ++i) {
  115. this.last = lastPropertyIndex === i;
  116. node = block.nodes[i];
  117. switch (node.nodeName) {
  118. case 'null':
  119. case 'expression':
  120. case 'function':
  121. case 'group':
  122. case 'block':
  123. case 'unit':
  124. case 'media':
  125. case 'keyframes':
  126. case 'atrule':
  127. case 'supports':
  128. continue;
  129. // inline comments
  130. case !this.compress && node.inline && 'comment':
  131. this.buf = this.buf.slice(0, -1);
  132. this.buf += this.out(' ' + this.visit(node) + '\n', node);
  133. break;
  134. case 'property':
  135. var ret = this.visit(node) + separator;
  136. this.buf += this.compress ? ret : this.out(ret, node);
  137. break;
  138. default:
  139. this.buf += this.out(this.visit(node) + separator, node);
  140. }
  141. }
  142. if (needBrackets) {
  143. --this.indents;
  144. this.buf += this.out(this.indent + '}' + separator);
  145. }
  146. }
  147. // Nesting
  148. for (var i = 0, len = block.nodes.length; i < len; ++i) {
  149. node = block.nodes[i];
  150. switch (node.nodeName) {
  151. case 'group':
  152. case 'block':
  153. case 'keyframes':
  154. if (this.linenos || this.firebug) this.debugInfo(node);
  155. this.visit(node);
  156. break;
  157. case 'media':
  158. case 'import':
  159. case 'atrule':
  160. case 'supports':
  161. this.visit(node);
  162. break;
  163. case 'comment':
  164. // only show unsuppressed comments
  165. if (!node.suppress) {
  166. this.buf += this.out(this.indent + this.visit(node) + '\n', node);
  167. }
  168. break;
  169. case 'charset':
  170. case 'literal':
  171. case 'namespace':
  172. this.buf += this.out(this.visit(node) + '\n', node);
  173. break;
  174. }
  175. }
  176. };
  177. /**
  178. * Visit Keyframes.
  179. */
  180. Compiler.prototype.visitKeyframes = function(node){
  181. if (!node.frames) return;
  182. var prefix = 'official' == node.prefix
  183. ? ''
  184. : '-' + node.prefix + '-';
  185. this.buf += this.out('@' + prefix + 'keyframes '
  186. + this.visit(node.val)
  187. + (this.compress ? '{' : ' {\n'), node);
  188. this.keyframe = true;
  189. ++this.indents;
  190. this.visit(node.block);
  191. --this.indents;
  192. this.keyframe = false;
  193. this.buf += this.out('}' + (this.compress ? '' : '\n'));
  194. };
  195. /**
  196. * Visit Media.
  197. */
  198. Compiler.prototype.visitMedia = function(media){
  199. var val = media.val;
  200. if (!media.hasOutput || !val.nodes.length) return;
  201. this.buf += this.out('@media ', media);
  202. this.visit(val);
  203. this.buf += this.out(this.compress ? '{' : ' {\n');
  204. ++this.indents;
  205. this.visit(media.block);
  206. --this.indents;
  207. this.buf += this.out('}' + (this.compress ? '' : '\n'));
  208. };
  209. /**
  210. * Visit QueryList.
  211. */
  212. Compiler.prototype.visitQueryList = function(queries){
  213. for (var i = 0, len = queries.nodes.length; i < len; ++i) {
  214. this.visit(queries.nodes[i]);
  215. if (len - 1 != i) this.buf += this.out(',' + (this.compress ? '' : ' '));
  216. }
  217. };
  218. /**
  219. * Visit Query.
  220. */
  221. Compiler.prototype.visitQuery = function(node){
  222. var len = node.nodes.length;
  223. if (node.predicate) this.buf += this.out(node.predicate + ' ');
  224. if (node.type) this.buf += this.out(node.type + (len ? ' and ' : ''));
  225. for (var i = 0; i < len; ++i) {
  226. this.buf += this.out(this.visit(node.nodes[i]));
  227. if (len - 1 != i) this.buf += this.out(' and ');
  228. }
  229. };
  230. /**
  231. * Visit Feature.
  232. */
  233. Compiler.prototype.visitFeature = function(node){
  234. if (!node.expr) {
  235. return node.name;
  236. } else if (node.expr.isEmpty) {
  237. return '(' + node.name + ')';
  238. } else {
  239. return '(' + node.name + ':' + (this.compress ? '' : ' ') + this.visit(node.expr) + ')';
  240. }
  241. };
  242. /**
  243. * Visit Import.
  244. */
  245. Compiler.prototype.visitImport = function(imported){
  246. this.buf += this.out('@import ' + this.visit(imported.path) + ';\n', imported);
  247. };
  248. /**
  249. * Visit Atrule.
  250. */
  251. Compiler.prototype.visitAtrule = function(atrule){
  252. var newline = this.compress ? '' : '\n';
  253. this.buf += this.out(this.indent + '@' + atrule.type, atrule);
  254. if (atrule.val) this.buf += this.out(' ' + atrule.val.trim());
  255. if (atrule.block) {
  256. if (atrule.block.isEmpty) {
  257. this.buf += this.out((this.compress ? '' : ' ') + '{}' + newline);
  258. } else if (atrule.hasOnlyProperties) {
  259. this.visit(atrule.block);
  260. } else {
  261. this.buf += this.out(this.compress ? '{' : ' {\n');
  262. ++this.indents;
  263. this.visit(atrule.block);
  264. --this.indents;
  265. this.buf += this.out(this.indent + '}' + newline);
  266. }
  267. } else {
  268. this.buf += this.out(';' + newline);
  269. }
  270. };
  271. /**
  272. * Visit Supports.
  273. */
  274. Compiler.prototype.visitSupports = function(node){
  275. if (!node.hasOutput) return;
  276. this.buf += this.out(this.indent + '@supports ', node);
  277. this.isCondition = true;
  278. this.buf += this.out(this.visit(node.condition));
  279. this.isCondition = false;
  280. this.buf += this.out(this.compress ? '{' : ' {\n');
  281. ++this.indents;
  282. this.visit(node.block);
  283. --this.indents;
  284. this.buf += this.out(this.indent + '}' + (this.compress ? '' : '\n'));
  285. },
  286. /**
  287. * Visit Comment.
  288. */
  289. Compiler.prototype.visitComment = function(comment){
  290. return this.compress
  291. ? comment.suppress
  292. ? ''
  293. : comment.str
  294. : comment.str;
  295. };
  296. /**
  297. * Visit Function.
  298. */
  299. Compiler.prototype.visitFunction = function(fn){
  300. return fn.name;
  301. };
  302. /**
  303. * Visit Charset.
  304. */
  305. Compiler.prototype.visitCharset = function(charset){
  306. return '@charset ' + this.visit(charset.val) + ';';
  307. };
  308. /**
  309. * Visit Namespace.
  310. */
  311. Compiler.prototype.visitNamespace = function(namespace){
  312. return '@namespace '
  313. + (namespace.prefix ? this.visit(namespace.prefix) + ' ' : '')
  314. + this.visit(namespace.val) + ';';
  315. };
  316. /**
  317. * Visit Literal.
  318. */
  319. Compiler.prototype.visitLiteral = function(lit){
  320. var val = lit.val;
  321. if (lit.css) val = val.replace(/^ /gm, '');
  322. return val;
  323. };
  324. /**
  325. * Visit Boolean.
  326. */
  327. Compiler.prototype.visitBoolean = function(bool){
  328. return bool.toString();
  329. };
  330. /**
  331. * Visit RGBA.
  332. */
  333. Compiler.prototype.visitRGBA = function(rgba){
  334. return rgba.toString();
  335. };
  336. /**
  337. * Visit HSLA.
  338. */
  339. Compiler.prototype.visitHSLA = function(hsla){
  340. return hsla.rgba.toString();
  341. };
  342. /**
  343. * Visit Unit.
  344. */
  345. Compiler.prototype.visitUnit = function(unit){
  346. var type = unit.type || ''
  347. , n = unit.val
  348. , float = n != (n | 0);
  349. // Compress
  350. if (this.compress) {
  351. // Always return '0' unless the unit is a percentage, time, degree or fraction
  352. if (!(['%', 's', 'ms', 'deg', 'fr'].includes(type)) && 0 == n) return '0';
  353. // Omit leading '0' on floats
  354. if (float && n < 1 && n > -1) {
  355. return n.toString().replace('0.', '.') + type;
  356. }
  357. }
  358. return (float ? parseFloat(n.toFixed(15)) : n).toString() + type;
  359. };
  360. /**
  361. * Visit Group.
  362. */
  363. Compiler.prototype.visitGroup = function(group){
  364. var stack = this.keyframe ? [] : this.stack
  365. , comma = this.compress ? ',' : ',\n';
  366. stack.push(group.nodes);
  367. // selectors
  368. if (group.block.hasProperties) {
  369. var selectors = utils.compileSelectors.call(this, stack)
  370. , len = selectors.length;
  371. if (len) {
  372. if (this.keyframe) comma = this.compress ? ',' : ', ';
  373. for (var i = 0; i < len; ++i) {
  374. var selector = selectors[i]
  375. , last = (i == len - 1);
  376. // keyframe blocks (10%, 20% { ... })
  377. if (this.keyframe) selector = i ? selector.trim() : selector;
  378. this.buf += this.out(selector + (last ? '' : comma), group.nodes[i]);
  379. }
  380. } else {
  381. group.block.lacksRenderedSelectors = true;
  382. }
  383. }
  384. // output block
  385. this.visit(group.block);
  386. stack.pop();
  387. };
  388. /**
  389. * Visit Ident.
  390. */
  391. Compiler.prototype.visitIdent = function(ident){
  392. return ident.name;
  393. };
  394. /**
  395. * Visit String.
  396. */
  397. Compiler.prototype.visitString = function(string){
  398. return this.isURL
  399. ? string.val
  400. : string.toString();
  401. };
  402. /**
  403. * Visit Null.
  404. */
  405. Compiler.prototype.visitNull = function(node){
  406. return '';
  407. };
  408. /**
  409. * Visit Call.
  410. */
  411. Compiler.prototype.visitCall = function(call){
  412. this.isURL = 'url' == call.name;
  413. var args = call.args.nodes.map(function(arg){
  414. return this.visit(arg);
  415. }, this).join(this.compress ? ',' : ', ');
  416. if (this.isURL) args = '"' + args + '"';
  417. this.isURL = false;
  418. return call.name + '(' + args + ')';
  419. };
  420. /**
  421. * Visit Expression.
  422. */
  423. Compiler.prototype.visitExpression = function(expr){
  424. var buf = []
  425. , self = this
  426. , len = expr.nodes.length
  427. , nodes = expr.nodes.map(function(node){ return self.visit(node); });
  428. nodes.forEach(function(node, i){
  429. var last = i == len - 1;
  430. buf.push(node);
  431. if ('/' == nodes[i + 1] || '/' == node) return;
  432. if (last) return;
  433. var space = self.isURL || (self.isCondition
  434. && (')' == nodes[i + 1] || '(' == node))
  435. ? '' : ' ';
  436. buf.push(expr.isList
  437. ? (self.compress ? ',' : ', ')
  438. : space);
  439. });
  440. return buf.join('');
  441. };
  442. /**
  443. * Visit Arguments.
  444. */
  445. Compiler.prototype.visitArguments = Compiler.prototype.visitExpression;
  446. /**
  447. * Visit Property.
  448. */
  449. Compiler.prototype.visitProperty = function(prop){
  450. var val = this.visit(prop.expr).trim()
  451. , name = (prop.name || prop.segments.join(''))
  452. , arr = [];
  453. if (name === '@apply') {
  454. arr.push(
  455. this.out(this.indent),
  456. this.out(name + ' ', prop),
  457. this.out(val, prop.expr),
  458. this.out(this.compress ? (this.last ? '' : ';') : ';')
  459. );
  460. return arr.join('');
  461. }
  462. arr.push(
  463. this.out(this.indent),
  464. this.out(name + (this.compress ? ':' : ': '), prop),
  465. this.out(val, prop.expr),
  466. this.out(this.compress ? (this.last ? '' : ';') : ';')
  467. );
  468. return arr.join('');
  469. };
  470. /**
  471. * Debug info.
  472. */
  473. Compiler.prototype.debugInfo = function(node){
  474. var path = node.filename == 'stdin' ? 'stdin' : fs.realpathSync(node.filename)
  475. , line = (node.nodes && node.nodes.length ? node.nodes[0].lineno : node.lineno) || 1;
  476. if (this.linenos){
  477. this.buf += '\n/* ' + 'line ' + line + ' : ' + path + ' */\n';
  478. }
  479. if (this.firebug){
  480. // debug info for firebug, the crazy formatting is needed
  481. path = 'file\\\:\\\/\\\/' + path.replace(/([.:/\\])/g, function(m) {
  482. return '\\' + (m === '\\' ? '\/' : m)
  483. });
  484. line = '\\00003' + line;
  485. this.buf += '\n@media -stylus-debug-info'
  486. + '{filename{font-family:' + path
  487. + '}line{font-family:' + line + '}}\n';
  488. }
  489. }