compiler.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  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. if (block.hasProperties && !block.lacksRenderedSelectors) {
  100. needBrackets = this.needBrackets(block.node);
  101. if (needBrackets) {
  102. this.buf += this.out(this.compress ? '{' : ' {\n');
  103. ++this.indents;
  104. }
  105. for (var i = 0, len = block.nodes.length; i < len; ++i) {
  106. this.last = len - 1 == i;
  107. node = block.nodes[i];
  108. switch (node.nodeName) {
  109. case 'null':
  110. case 'expression':
  111. case 'function':
  112. case 'group':
  113. case 'block':
  114. case 'unit':
  115. case 'media':
  116. case 'keyframes':
  117. case 'atrule':
  118. case 'supports':
  119. continue;
  120. // inline comments
  121. case !this.compress && node.inline && 'comment':
  122. this.buf = this.buf.slice(0, -1);
  123. this.buf += this.out(' ' + this.visit(node) + '\n', node);
  124. break;
  125. case 'property':
  126. var ret = this.visit(node) + separator;
  127. this.buf += this.compress ? ret : this.out(ret, node);
  128. break;
  129. default:
  130. this.buf += this.out(this.visit(node) + separator, node);
  131. }
  132. }
  133. if (needBrackets) {
  134. --this.indents;
  135. this.buf += this.out(this.indent + '}' + separator);
  136. }
  137. }
  138. // Nesting
  139. for (var i = 0, len = block.nodes.length; i < len; ++i) {
  140. node = block.nodes[i];
  141. switch (node.nodeName) {
  142. case 'group':
  143. case 'block':
  144. case 'keyframes':
  145. if (this.linenos || this.firebug) this.debugInfo(node);
  146. this.visit(node);
  147. break;
  148. case 'media':
  149. case 'import':
  150. case 'atrule':
  151. case 'supports':
  152. this.visit(node);
  153. break;
  154. case 'comment':
  155. // only show unsuppressed comments
  156. if (!node.suppress) {
  157. this.buf += this.out(this.indent + this.visit(node) + '\n', node);
  158. }
  159. break;
  160. case 'charset':
  161. case 'literal':
  162. case 'namespace':
  163. this.buf += this.out(this.visit(node) + '\n', node);
  164. break;
  165. }
  166. }
  167. };
  168. /**
  169. * Visit Keyframes.
  170. */
  171. Compiler.prototype.visitKeyframes = function(node){
  172. if (!node.frames) return;
  173. var prefix = 'official' == node.prefix
  174. ? ''
  175. : '-' + node.prefix + '-';
  176. this.buf += this.out('@' + prefix + 'keyframes '
  177. + this.visit(node.val)
  178. + (this.compress ? '{' : ' {\n'), node);
  179. this.keyframe = true;
  180. ++this.indents;
  181. this.visit(node.block);
  182. --this.indents;
  183. this.keyframe = false;
  184. this.buf += this.out('}' + (this.compress ? '' : '\n'));
  185. };
  186. /**
  187. * Visit Media.
  188. */
  189. Compiler.prototype.visitMedia = function(media){
  190. var val = media.val;
  191. if (!media.hasOutput || !val.nodes.length) return;
  192. this.buf += this.out('@media ', media);
  193. this.visit(val);
  194. this.buf += this.out(this.compress ? '{' : ' {\n');
  195. ++this.indents;
  196. this.visit(media.block);
  197. --this.indents;
  198. this.buf += this.out('}' + (this.compress ? '' : '\n'));
  199. };
  200. /**
  201. * Visit QueryList.
  202. */
  203. Compiler.prototype.visitQueryList = function(queries){
  204. for (var i = 0, len = queries.nodes.length; i < len; ++i) {
  205. this.visit(queries.nodes[i]);
  206. if (len - 1 != i) this.buf += this.out(',' + (this.compress ? '' : ' '));
  207. }
  208. };
  209. /**
  210. * Visit Query.
  211. */
  212. Compiler.prototype.visitQuery = function(node){
  213. var len = node.nodes.length;
  214. if (node.predicate) this.buf += this.out(node.predicate + ' ');
  215. if (node.type) this.buf += this.out(node.type + (len ? ' and ' : ''));
  216. for (var i = 0; i < len; ++i) {
  217. this.buf += this.out(this.visit(node.nodes[i]));
  218. if (len - 1 != i) this.buf += this.out(' and ');
  219. }
  220. };
  221. /**
  222. * Visit Feature.
  223. */
  224. Compiler.prototype.visitFeature = function(node){
  225. if (!node.expr) {
  226. return node.name;
  227. } else if (node.expr.isEmpty) {
  228. return '(' + node.name + ')';
  229. } else {
  230. return '(' + node.name + ':' + (this.compress ? '' : ' ') + this.visit(node.expr) + ')';
  231. }
  232. };
  233. /**
  234. * Visit Import.
  235. */
  236. Compiler.prototype.visitImport = function(imported){
  237. this.buf += this.out('@import ' + this.visit(imported.path) + ';\n', imported);
  238. };
  239. /**
  240. * Visit Atrule.
  241. */
  242. Compiler.prototype.visitAtrule = function(atrule){
  243. var newline = this.compress ? '' : '\n';
  244. this.buf += this.out(this.indent + '@' + atrule.type, atrule);
  245. if (atrule.val) this.buf += this.out(' ' + atrule.val.trim());
  246. if (atrule.block) {
  247. if (atrule.hasOnlyProperties) {
  248. this.visit(atrule.block);
  249. } else {
  250. this.buf += this.out(this.compress ? '{' : ' {\n');
  251. ++this.indents;
  252. this.visit(atrule.block);
  253. --this.indents;
  254. this.buf += this.out(this.indent + '}' + newline);
  255. }
  256. } else {
  257. this.buf += this.out(';' + newline);
  258. }
  259. };
  260. /**
  261. * Visit Supports.
  262. */
  263. Compiler.prototype.visitSupports = function(node){
  264. if (!node.hasOutput) return;
  265. this.buf += this.out(this.indent + '@supports ', node);
  266. this.isCondition = true;
  267. this.buf += this.out(this.visit(node.condition));
  268. this.isCondition = false;
  269. this.buf += this.out(this.compress ? '{' : ' {\n');
  270. ++this.indents;
  271. this.visit(node.block);
  272. --this.indents;
  273. this.buf += this.out(this.indent + '}' + (this.compress ? '' : '\n'));
  274. },
  275. /**
  276. * Visit Comment.
  277. */
  278. Compiler.prototype.visitComment = function(comment){
  279. return this.compress
  280. ? comment.suppress
  281. ? ''
  282. : comment.str
  283. : comment.str;
  284. };
  285. /**
  286. * Visit Function.
  287. */
  288. Compiler.prototype.visitFunction = function(fn){
  289. return fn.name;
  290. };
  291. /**
  292. * Visit Charset.
  293. */
  294. Compiler.prototype.visitCharset = function(charset){
  295. return '@charset ' + this.visit(charset.val) + ';';
  296. };
  297. /**
  298. * Visit Namespace.
  299. */
  300. Compiler.prototype.visitNamespace = function(namespace){
  301. return '@namespace '
  302. + (namespace.prefix ? this.visit(namespace.prefix) + ' ' : '')
  303. + this.visit(namespace.val) + ';';
  304. };
  305. /**
  306. * Visit Literal.
  307. */
  308. Compiler.prototype.visitLiteral = function(lit){
  309. var val = lit.val;
  310. if (lit.css) val = val.replace(/^ /gm, '');
  311. return val;
  312. };
  313. /**
  314. * Visit Boolean.
  315. */
  316. Compiler.prototype.visitBoolean = function(bool){
  317. return bool.toString();
  318. };
  319. /**
  320. * Visit RGBA.
  321. */
  322. Compiler.prototype.visitRGBA = function(rgba){
  323. return rgba.toString();
  324. };
  325. /**
  326. * Visit HSLA.
  327. */
  328. Compiler.prototype.visitHSLA = function(hsla){
  329. return hsla.rgba.toString();
  330. };
  331. /**
  332. * Visit Unit.
  333. */
  334. Compiler.prototype.visitUnit = function(unit){
  335. var type = unit.type || ''
  336. , n = unit.val
  337. , float = n != (n | 0);
  338. // Compress
  339. if (this.compress) {
  340. // Always return '0' unless the unit is a percentage or time
  341. if ('%' != type && 's' != type && 'ms' != type && 0 == n) return '0';
  342. // Omit leading '0' on floats
  343. if (float && n < 1 && n > -1) {
  344. return n.toString().replace('0.', '.') + type;
  345. }
  346. }
  347. return (float ? parseFloat(n.toFixed(15)) : n).toString() + type;
  348. };
  349. /**
  350. * Visit Group.
  351. */
  352. Compiler.prototype.visitGroup = function(group){
  353. var stack = this.keyframe ? [] : this.stack
  354. , comma = this.compress ? ',' : ',\n';
  355. stack.push(group.nodes);
  356. // selectors
  357. if (group.block.hasProperties) {
  358. var selectors = utils.compileSelectors.call(this, stack)
  359. , len = selectors.length;
  360. if (len) {
  361. if (this.keyframe) comma = this.compress ? ',' : ', ';
  362. for (var i = 0; i < len; ++i) {
  363. var selector = selectors[i]
  364. , last = (i == len - 1);
  365. // keyframe blocks (10%, 20% { ... })
  366. if (this.keyframe) selector = i ? selector.trim() : selector;
  367. this.buf += this.out(selector + (last ? '' : comma), group.nodes[i]);
  368. }
  369. } else {
  370. group.block.lacksRenderedSelectors = true;
  371. }
  372. }
  373. // output block
  374. this.visit(group.block);
  375. stack.pop();
  376. };
  377. /**
  378. * Visit Ident.
  379. */
  380. Compiler.prototype.visitIdent = function(ident){
  381. return ident.name;
  382. };
  383. /**
  384. * Visit String.
  385. */
  386. Compiler.prototype.visitString = function(string){
  387. return this.isURL
  388. ? string.val
  389. : string.toString();
  390. };
  391. /**
  392. * Visit Null.
  393. */
  394. Compiler.prototype.visitNull = function(node){
  395. return '';
  396. };
  397. /**
  398. * Visit Call.
  399. */
  400. Compiler.prototype.visitCall = function(call){
  401. this.isURL = 'url' == call.name;
  402. var args = call.args.nodes.map(function(arg){
  403. return this.visit(arg);
  404. }, this).join(this.compress ? ',' : ', ');
  405. if (this.isURL) args = '"' + args + '"';
  406. this.isURL = false;
  407. return call.name + '(' + args + ')';
  408. };
  409. /**
  410. * Visit Expression.
  411. */
  412. Compiler.prototype.visitExpression = function(expr){
  413. var buf = []
  414. , self = this
  415. , len = expr.nodes.length
  416. , nodes = expr.nodes.map(function(node){ return self.visit(node); });
  417. nodes.forEach(function(node, i){
  418. var last = i == len - 1;
  419. buf.push(node);
  420. if ('/' == nodes[i + 1] || '/' == node) return;
  421. if (last) return;
  422. var space = self.isURL || (self.isCondition
  423. && (')' == nodes[i + 1] || '(' == node))
  424. ? '' : ' ';
  425. buf.push(expr.isList
  426. ? (self.compress ? ',' : ', ')
  427. : space);
  428. });
  429. return buf.join('');
  430. };
  431. /**
  432. * Visit Arguments.
  433. */
  434. Compiler.prototype.visitArguments = Compiler.prototype.visitExpression;
  435. /**
  436. * Visit Property.
  437. */
  438. Compiler.prototype.visitProperty = function(prop){
  439. var val = this.visit(prop.expr).trim()
  440. , name = (prop.name || prop.segments.join(''))
  441. , arr = [];
  442. arr.push(
  443. this.out(this.indent),
  444. this.out(name + (this.compress ? ':' : ': '), prop),
  445. this.out(val, prop.expr),
  446. this.out(this.compress ? (this.last ? '' : ';') : ';')
  447. );
  448. return arr.join('');
  449. };
  450. /**
  451. * Debug info.
  452. */
  453. Compiler.prototype.debugInfo = function(node){
  454. var path = node.filename == 'stdin' ? 'stdin' : fs.realpathSync(node.filename)
  455. , line = (node.nodes && node.nodes.length ? node.nodes[0].lineno : node.lineno) || 1;
  456. if (this.linenos){
  457. this.buf += '\n/* ' + 'line ' + line + ' : ' + path + ' */\n';
  458. }
  459. if (this.firebug){
  460. // debug info for firebug, the crazy formatting is needed
  461. path = 'file\\\:\\\/\\\/' + path.replace(/([.:/\\])/g, function(m) {
  462. return '\\' + (m === '\\' ? '\/' : m)
  463. });
  464. line = '\\00003' + line;
  465. this.buf += '\n@media -stylus-debug-info'
  466. + '{filename{font-family:' + path
  467. + '}line{font-family:' + line + '}}\n';
  468. }
  469. }