parser.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. var DEBUG = false; // `true` to print debugging info.
  2. var TIMER = false; // `true` to time calls to `parse()` and print the results.
  3. var debug = require('./debug')('parse');
  4. var lex = require('./lexer');
  5. exports = module.exports = parse;
  6. var _comments; // Whether comments are allowed.
  7. var _depth; // Current block nesting depth.
  8. var _position; // Whether to include line/column position.
  9. var _tokens; // Array of lexical tokens.
  10. /**
  11. * Convert a CSS string or array of lexical tokens into a `stringify`-able AST.
  12. *
  13. * @param {String} css CSS string or array of lexical token
  14. * @param {Object} [options]
  15. * @param {Boolean} [options.comments=false] allow comment nodes in the AST
  16. * @returns {Object} `stringify`-able AST
  17. */
  18. function parse(css, options) {
  19. var start; // Debug timer start.
  20. options || (options = {});
  21. _comments = !!options.comments;
  22. _position = !!options.position;
  23. _depth = 0;
  24. // Operate on a copy of the given tokens, or the lex()'d CSS string.
  25. _tokens = Array.isArray(css) ? css.slice() : lex(css);
  26. var rule;
  27. var rules = [];
  28. var token;
  29. TIMER && (start = Date.now());
  30. while ((token = next())) {
  31. rule = parseToken(token);
  32. rule && rules.push(rule);
  33. }
  34. TIMER && debug('ran in', (Date.now() - start) + 'ms');
  35. return {
  36. type: "stylesheet",
  37. stylesheet: {
  38. rules: rules
  39. }
  40. };
  41. }
  42. // -- Functions --------------------------------------------------------------
  43. /**
  44. * Build an AST node from a lexical token.
  45. *
  46. * @param {Object} token lexical token
  47. * @param {Object} [override] object hash of properties that override those
  48. * already in the token, or that will be added to the token.
  49. * @returns {Object} AST node
  50. */
  51. function astNode(token, override) {
  52. override || (override = {});
  53. var key;
  54. var keys = ['type', 'name', 'value'];
  55. var node = {};
  56. // Avoiding [].forEach for performance reasons.
  57. for (var i = 0; i < keys.length; ++i) {
  58. key = keys[i];
  59. if (token[key]) {
  60. node[key] = override[key] || token[key];
  61. }
  62. }
  63. keys = Object.keys(override);
  64. for (i = 0; i < keys.length; ++i) {
  65. key = keys[i];
  66. if (!node[key]) {
  67. node[key] = override[key];
  68. }
  69. }
  70. if (_position) {
  71. node.position = {
  72. start: token.start,
  73. end: token.end
  74. };
  75. }
  76. DEBUG && debug('astNode:', JSON.stringify(node, null, 2));
  77. return node;
  78. }
  79. /**
  80. * Remove a lexical token from the stack and return the removed token.
  81. *
  82. * @returns {Object} lexical token
  83. */
  84. function next() {
  85. var token = _tokens.shift();
  86. DEBUG && debug('next:', JSON.stringify(token, null, 2));
  87. return token;
  88. }
  89. // -- Parse* Functions ---------------------------------------------------------
  90. /**
  91. * Convert an @-group lexical token to an AST node.
  92. *
  93. * @param {Object} token @-group lexical token
  94. * @returns {Object} @-group AST node
  95. */
  96. function parseAtGroup(token) {
  97. _depth = _depth + 1;
  98. // As the @-group token is assembled, relevant token values are captured here
  99. // temporarily. They will later be used as `tokenize()` overrides.
  100. var overrides = {};
  101. switch (token.type) {
  102. case 'font-face':
  103. case 'viewport' :
  104. overrides.declarations = parseDeclarations();
  105. break;
  106. case 'page':
  107. overrides.prefix = token.prefix;
  108. overrides.declarations = parseDeclarations();
  109. break;
  110. default:
  111. overrides.prefix = token.prefix;
  112. overrides.rules = parseRules();
  113. }
  114. return astNode(token, overrides);
  115. }
  116. /**
  117. * Convert an @import lexical token to an AST node.
  118. *
  119. * @param {Object} token @import lexical token
  120. * @returns {Object} @import AST node
  121. */
  122. function parseAtImport(token) {
  123. return astNode(token);
  124. }
  125. /**
  126. * Convert an @charset token to an AST node.
  127. *
  128. * @param {Object} token @charset lexical token
  129. * @returns {Object} @charset node
  130. */
  131. function parseCharset(token) {
  132. return astNode(token);
  133. }
  134. /**
  135. * Convert a comment token to an AST Node.
  136. *
  137. * @param {Object} token comment lexical token
  138. * @returns {Object} comment node
  139. */
  140. function parseComment(token) {
  141. return astNode(token, {text: token.text});
  142. }
  143. function parseNamespace(token) {
  144. return astNode(token);
  145. }
  146. /**
  147. * Convert a property lexical token to a property AST node.
  148. *
  149. * @returns {Object} property node
  150. */
  151. function parseProperty(token) {
  152. return astNode(token);
  153. }
  154. /**
  155. * Convert a selector lexical token to a selector AST node.
  156. *
  157. * @param {Object} token selector lexical token
  158. * @returns {Object} selector node
  159. */
  160. function parseSelector(token) {
  161. function trim(str) {
  162. return str.trim();
  163. }
  164. return astNode(token, {
  165. type: 'rule',
  166. selectors: token.text.split(',').map(trim),
  167. declarations: parseDeclarations(token)
  168. });
  169. }
  170. /**
  171. * Convert a lexical token to an AST node.
  172. *
  173. * @returns {Object|undefined} AST node
  174. */
  175. function parseToken(token) {
  176. switch (token.type) {
  177. // Cases are listed in roughly descending order of probability.
  178. case 'property': return parseProperty(token);
  179. case 'selector': return parseSelector(token);
  180. case 'at-group-end': _depth = _depth - 1; return;
  181. case 'media' :
  182. case 'keyframes' :return parseAtGroup(token);
  183. case 'comment': if (_comments) { return parseComment(token); } break;
  184. case 'charset': return parseCharset(token);
  185. case 'import': return parseAtImport(token);
  186. case 'namespace': return parseNamespace(token);
  187. case 'font-face':
  188. case 'supports' :
  189. case 'viewport' :
  190. case 'document' :
  191. case 'page' : return parseAtGroup(token);
  192. }
  193. DEBUG && debug('parseToken: unexpected token:', JSON.stringify(token));
  194. }
  195. // -- Parse Helper Functions ---------------------------------------------------
  196. /**
  197. * Iteratively parses lexical tokens from the stack into AST nodes until a
  198. * conditional function returns `false`, at which point iteration terminates
  199. * and any AST nodes collected are returned.
  200. *
  201. * @param {Function} conditionFn
  202. * @param {Object} token the lexical token being parsed
  203. * @returns {Boolean} `true` if the token should be parsed, `false` otherwise
  204. * @return {Array} AST nodes
  205. */
  206. function parseTokensWhile(conditionFn) {
  207. var node;
  208. var nodes = [];
  209. var token;
  210. while ((token = next()) && (conditionFn && conditionFn(token))) {
  211. node = parseToken(token);
  212. node && nodes.push(node);
  213. }
  214. // Place an unused non-`end` lexical token back onto the stack.
  215. if (token && token.type !== 'end') {
  216. _tokens.unshift(token);
  217. }
  218. return nodes;
  219. }
  220. /**
  221. * Convert a series of tokens into a sequence of declaration AST nodes.
  222. *
  223. * @returns {Array} declaration nodes
  224. */
  225. function parseDeclarations() {
  226. return parseTokensWhile(function (token) {
  227. return (token.type === 'property' || token.type === 'comment');
  228. });
  229. }
  230. /**
  231. * Convert a series of tokens into a sequence of rule nodes.
  232. *
  233. * @returns {Array} rule nodes
  234. */
  235. function parseRules() {
  236. return parseTokensWhile(function () { return _depth; });
  237. }