123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291 |
- var DEBUG = false; // `true` to print debugging info.
- var TIMER = false; // `true` to time calls to `parse()` and print the results.
- var debug = require('./debug')('parse');
- var lex = require('./lexer');
- exports = module.exports = parse;
- var _comments; // Whether comments are allowed.
- var _depth; // Current block nesting depth.
- var _position; // Whether to include line/column position.
- var _tokens; // Array of lexical tokens.
- /**
- * Convert a CSS string or array of lexical tokens into a `stringify`-able AST.
- *
- * @param {String} css CSS string or array of lexical token
- * @param {Object} [options]
- * @param {Boolean} [options.comments=false] allow comment nodes in the AST
- * @returns {Object} `stringify`-able AST
- */
- function parse(css, options) {
- var start; // Debug timer start.
- options || (options = {});
- _comments = !!options.comments;
- _position = !!options.position;
- _depth = 0;
- // Operate on a copy of the given tokens, or the lex()'d CSS string.
- _tokens = Array.isArray(css) ? css.slice() : lex(css);
- var rule;
- var rules = [];
- var token;
- TIMER && (start = Date.now());
- while ((token = next())) {
- rule = parseToken(token);
- rule && rules.push(rule);
- }
- TIMER && debug('ran in', (Date.now() - start) + 'ms');
- return {
- type: "stylesheet",
- stylesheet: {
- rules: rules
- }
- };
- }
- // -- Functions --------------------------------------------------------------
- /**
- * Build an AST node from a lexical token.
- *
- * @param {Object} token lexical token
- * @param {Object} [override] object hash of properties that override those
- * already in the token, or that will be added to the token.
- * @returns {Object} AST node
- */
- function astNode(token, override) {
- override || (override = {});
- var key;
- var keys = ['type', 'name', 'value'];
- var node = {};
- // Avoiding [].forEach for performance reasons.
- for (var i = 0; i < keys.length; ++i) {
- key = keys[i];
- if (token[key]) {
- node[key] = override[key] || token[key];
- }
- }
- keys = Object.keys(override);
- for (i = 0; i < keys.length; ++i) {
- key = keys[i];
- if (!node[key]) {
- node[key] = override[key];
- }
- }
- if (_position) {
- node.position = {
- start: token.start,
- end: token.end
- };
- }
- DEBUG && debug('astNode:', JSON.stringify(node, null, 2));
- return node;
- }
- /**
- * Remove a lexical token from the stack and return the removed token.
- *
- * @returns {Object} lexical token
- */
- function next() {
- var token = _tokens.shift();
- DEBUG && debug('next:', JSON.stringify(token, null, 2));
- return token;
- }
- // -- Parse* Functions ---------------------------------------------------------
- /**
- * Convert an @-group lexical token to an AST node.
- *
- * @param {Object} token @-group lexical token
- * @returns {Object} @-group AST node
- */
- function parseAtGroup(token) {
- _depth = _depth + 1;
- // As the @-group token is assembled, relevant token values are captured here
- // temporarily. They will later be used as `tokenize()` overrides.
- var overrides = {};
- switch (token.type) {
- case 'font-face':
- case 'viewport' :
- overrides.declarations = parseDeclarations();
- break;
- case 'page':
- overrides.prefix = token.prefix;
- overrides.declarations = parseDeclarations();
- break;
- default:
- overrides.prefix = token.prefix;
- overrides.rules = parseRules();
- }
- return astNode(token, overrides);
- }
- /**
- * Convert an @import lexical token to an AST node.
- *
- * @param {Object} token @import lexical token
- * @returns {Object} @import AST node
- */
- function parseAtImport(token) {
- return astNode(token);
- }
- /**
- * Convert an @charset token to an AST node.
- *
- * @param {Object} token @charset lexical token
- * @returns {Object} @charset node
- */
- function parseCharset(token) {
- return astNode(token);
- }
- /**
- * Convert a comment token to an AST Node.
- *
- * @param {Object} token comment lexical token
- * @returns {Object} comment node
- */
- function parseComment(token) {
- return astNode(token, {text: token.text});
- }
- function parseNamespace(token) {
- return astNode(token);
- }
- /**
- * Convert a property lexical token to a property AST node.
- *
- * @returns {Object} property node
- */
- function parseProperty(token) {
- return astNode(token);
- }
- /**
- * Convert a selector lexical token to a selector AST node.
- *
- * @param {Object} token selector lexical token
- * @returns {Object} selector node
- */
- function parseSelector(token) {
- function trim(str) {
- return str.trim();
- }
- return astNode(token, {
- type: 'rule',
- selectors: token.text.split(',').map(trim),
- declarations: parseDeclarations(token)
- });
- }
- /**
- * Convert a lexical token to an AST node.
- *
- * @returns {Object|undefined} AST node
- */
- function parseToken(token) {
- switch (token.type) {
- // Cases are listed in roughly descending order of probability.
- case 'property': return parseProperty(token);
- case 'selector': return parseSelector(token);
- case 'at-group-end': _depth = _depth - 1; return;
- case 'media' :
- case 'keyframes' :return parseAtGroup(token);
- case 'comment': if (_comments) { return parseComment(token); } break;
- case 'charset': return parseCharset(token);
- case 'import': return parseAtImport(token);
- case 'namespace': return parseNamespace(token);
- case 'font-face':
- case 'supports' :
- case 'viewport' :
- case 'document' :
- case 'page' : return parseAtGroup(token);
- }
- DEBUG && debug('parseToken: unexpected token:', JSON.stringify(token));
- }
- // -- Parse Helper Functions ---------------------------------------------------
- /**
- * Iteratively parses lexical tokens from the stack into AST nodes until a
- * conditional function returns `false`, at which point iteration terminates
- * and any AST nodes collected are returned.
- *
- * @param {Function} conditionFn
- * @param {Object} token the lexical token being parsed
- * @returns {Boolean} `true` if the token should be parsed, `false` otherwise
- * @return {Array} AST nodes
- */
- function parseTokensWhile(conditionFn) {
- var node;
- var nodes = [];
- var token;
- while ((token = next()) && (conditionFn && conditionFn(token))) {
- node = parseToken(token);
- node && nodes.push(node);
- }
- // Place an unused non-`end` lexical token back onto the stack.
- if (token && token.type !== 'end') {
- _tokens.unshift(token);
- }
- return nodes;
- }
- /**
- * Convert a series of tokens into a sequence of declaration AST nodes.
- *
- * @returns {Array} declaration nodes
- */
- function parseDeclarations() {
- return parseTokensWhile(function (token) {
- return (token.type === 'property' || token.type === 'comment');
- });
- }
- /**
- * Convert a series of tokens into a sequence of rule nodes.
- *
- * @returns {Array} rule nodes
- */
- function parseRules() {
- return parseTokensWhile(function () { return _depth; });
- }
|