259 lines
6.3 KiB
JavaScript
259 lines
6.3 KiB
JavaScript
var DEBUG = false; // `true` to print debugging info.
|
|
var TIMER = false; // `true` to time calls to `stringify()` and print the results.
|
|
|
|
var debug = require('./debug')('stringify');
|
|
|
|
var _comments; // Whether comments are allowed in the stringified CSS.
|
|
var _compress; // Whether the stringified CSS should be compressed.
|
|
var _indentation; // Indentation option value.
|
|
var _level; // Current indentation level.
|
|
var _n; // Compression-aware newline character.
|
|
var _s; // Compression-aware space character.
|
|
|
|
exports = module.exports = stringify;
|
|
|
|
/**
|
|
* Convert a `stringify`-able AST into a CSS string.
|
|
*
|
|
* @param {Object} `stringify`-able AST
|
|
* @param {Object} [options]
|
|
* @param {Boolean} [options.comments=false] allow comments in the CSS
|
|
* @param {Boolean} [options.compress=false] compress whitespace
|
|
* @param {String} [options.indentation=''] indentation sequence
|
|
* @returns {String} CSS
|
|
*/
|
|
function stringify(ast, options) {
|
|
var start; // Debug timer start.
|
|
|
|
options || (options = {});
|
|
_indentation = options.indentation || '';
|
|
_compress = !!options.compress;
|
|
_comments = !!options.comments;
|
|
_level = 1;
|
|
|
|
if (_compress) {
|
|
_n = _s = '';
|
|
} else {
|
|
_n = '\n';
|
|
_s = ' ';
|
|
}
|
|
|
|
TIMER && (start = Date.now());
|
|
|
|
var css = reduce(ast.stylesheet.rules, stringifyNode).join('\n').trim();
|
|
|
|
TIMER && debug('ran in', (Date.now() - start) + 'ms');
|
|
|
|
return css;
|
|
}
|
|
|
|
// -- Functions --------------------------------------------------------------
|
|
|
|
/**
|
|
* Modify the indentation level, or return a compression-aware sequence of
|
|
* spaces equal to the current indentation level.
|
|
*
|
|
* @param {Number} [level=undefined] indentation level modifier
|
|
* @returns {String} sequence of spaces
|
|
*/
|
|
function indent(level) {
|
|
if (level) {
|
|
_level += level;
|
|
return;
|
|
}
|
|
|
|
if (_compress) { return ''; }
|
|
|
|
return Array(_level).join(_indentation || '');
|
|
}
|
|
|
|
// -- Stringify Functions ------------------------------------------------------
|
|
|
|
/**
|
|
* Stringify an @-rule AST node.
|
|
*
|
|
* Use `stringifyAtGroup()` when dealing with @-groups that may contain blocks
|
|
* such as @media.
|
|
*
|
|
* @param {String} type @-rule type. E.g., import, charset
|
|
* @returns {String} Stringified @-rule
|
|
*/
|
|
function stringifyAtRule(node) {
|
|
return '@' + node.type + ' ' + node.value + ';' + _n;
|
|
}
|
|
|
|
/**
|
|
* Stringify an @-group AST node.
|
|
*
|
|
* Use `stringifyAtRule()` when dealing with @-rules that may not contain blocks
|
|
* such as @import.
|
|
*
|
|
* @param {Object} node @-group AST node
|
|
* @returns {String}
|
|
*/
|
|
function stringifyAtGroup(node) {
|
|
var label = '';
|
|
var prefix = node.prefix || '';
|
|
|
|
if (node.name) {
|
|
label = ' ' + node.name;
|
|
}
|
|
|
|
// FIXME: @-rule conditional logic is leaking everywhere.
|
|
var chomp = node.type !== 'page';
|
|
|
|
return '@' + prefix + node.type + label + _s + stringifyBlock(node, chomp) + _n;
|
|
}
|
|
|
|
/**
|
|
* Stringify a comment AST node.
|
|
*
|
|
* @param {Object} node comment AST node
|
|
* @returns {String}
|
|
*/
|
|
function stringifyComment(node) {
|
|
if (!_comments) { return ''; }
|
|
|
|
return '/*' + (node.text || '') + '*/' + _n;
|
|
}
|
|
|
|
/**
|
|
* Stringify a rule AST node.
|
|
*
|
|
* @param {Object} node rule AST node
|
|
* @returns {String}
|
|
*/
|
|
function stringifyRule(node) {
|
|
var label;
|
|
|
|
if (node.selectors) {
|
|
label = node.selectors.join(',' + _n);
|
|
} else {
|
|
label = '@' + node.type;
|
|
label += node.name ? ' ' + node.name : '';
|
|
}
|
|
|
|
return indent() + label + _s + stringifyBlock(node) + _n;
|
|
}
|
|
|
|
|
|
// -- Stringify Helper Functions -----------------------------------------------
|
|
|
|
/**
|
|
* Reduce an array by applying a function to each item and retaining the truthy
|
|
* results.
|
|
*
|
|
* When `item.type` is `'comment'` `stringifyComment` will be applied instead.
|
|
*
|
|
* @param {Array} items array to reduce
|
|
* @param {Function} fn function to call for each item in the array
|
|
* @returns {Mixed} Truthy values will be retained, falsy values omitted
|
|
* @returns {Array} retained results
|
|
*/
|
|
function reduce(items, fn) {
|
|
return items.reduce(function (results, item) {
|
|
var result = (item.type === 'comment') ? stringifyComment(item) : fn(item);
|
|
result && results.push(result);
|
|
return results;
|
|
}, []);
|
|
}
|
|
|
|
/**
|
|
* Stringify an AST node with the assumption that it represents a block of
|
|
* declarations or other @-group contents.
|
|
*
|
|
* @param {Object} node AST node
|
|
* @returns {String}
|
|
*/
|
|
// FIXME: chomp should not be a magic boolean parameter
|
|
function stringifyBlock(node, chomp) {
|
|
var children = node.declarations;
|
|
var fn = stringifyDeclaration;
|
|
|
|
if (node.rules) {
|
|
children = node.rules;
|
|
fn = stringifyRule;
|
|
}
|
|
|
|
children = stringifyChildren(children, fn);
|
|
children && (children = _n + children + (chomp ? '' : _n));
|
|
|
|
return '{' + children + indent() + '}';
|
|
}
|
|
|
|
/**
|
|
* Stringify an array of child AST nodes by calling the given stringify function
|
|
* once for each child, and concatenating the results.
|
|
*
|
|
* @param {Array} children `node.rules` or `node.declarations`
|
|
* @param {Function} fn stringify function
|
|
* @returns {String}
|
|
*/
|
|
function stringifyChildren(children, fn) {
|
|
if (!children) { return ''; }
|
|
|
|
indent(1);
|
|
var results = reduce(children, fn);
|
|
indent(-1);
|
|
|
|
if (!results.length) { return ''; }
|
|
|
|
return results.join(_n);
|
|
}
|
|
|
|
/**
|
|
* Stringify a declaration AST node.
|
|
*
|
|
* @param {Object} node declaration AST node
|
|
* @returns {String}
|
|
*/
|
|
function stringifyDeclaration(node) {
|
|
if (node.type === 'property') {
|
|
return stringifyProperty(node);
|
|
}
|
|
|
|
DEBUG && debug('stringifyDeclaration: unexpected node:', JSON.stringify(node));
|
|
}
|
|
|
|
/**
|
|
* Stringify an AST node.
|
|
*
|
|
* @param {Object} node AST node
|
|
* @returns {String}
|
|
*/
|
|
function stringifyNode(node) {
|
|
switch (node.type) {
|
|
// Cases are listed in roughly descending order of probability.
|
|
case 'rule': return stringifyRule(node);
|
|
|
|
case 'media' :
|
|
case 'keyframes': return stringifyAtGroup(node);
|
|
|
|
case 'comment': return stringifyComment(node);
|
|
|
|
case 'import' :
|
|
case 'charset' :
|
|
case 'namespace': return stringifyAtRule(node);
|
|
|
|
case 'font-face':
|
|
case 'supports' :
|
|
case 'viewport' :
|
|
case 'document' :
|
|
case 'page' : return stringifyAtGroup(node);
|
|
}
|
|
|
|
DEBUG && debug('stringifyNode: unexpected node: ' + JSON.stringify(node));
|
|
}
|
|
|
|
/**
|
|
* Stringify an AST property node.
|
|
*
|
|
* @param {Object} node AST property node
|
|
* @returns {String}
|
|
*/
|
|
function stringifyProperty(node) {
|
|
var name = node.name ? node.name + ':' + _s : '';
|
|
|
|
return indent() + name + node.value + ';';
|
|
}
|