fix
This commit is contained in:
11
book/node_modules/mensch/lib/debug.js
generated
vendored
Normal file
11
book/node_modules/mensch/lib/debug.js
generated
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
exports = module.exports = debug;
|
||||
|
||||
function debug(label) {
|
||||
return _debug.bind(null, label);
|
||||
}
|
||||
|
||||
function _debug(label) {
|
||||
var args = [].slice.call(arguments, 1);
|
||||
args.unshift('[' + label + ']');
|
||||
process.stderr.write(args.join(' ') + '\n');
|
||||
}
|
692
book/node_modules/mensch/lib/lexer.js
generated
vendored
Normal file
692
book/node_modules/mensch/lib/lexer.js
generated
vendored
Normal file
@ -0,0 +1,692 @@
|
||||
var DEBUG = false; // `true` to print debugging info.
|
||||
var TIMER = false; // `true` to time calls to `lex()` and print the results.
|
||||
|
||||
var debug = require('./debug')('lex');
|
||||
|
||||
exports = module.exports = lex;
|
||||
|
||||
/**
|
||||
* Convert a CSS string into an array of lexical tokens.
|
||||
*
|
||||
* @param {String} css CSS
|
||||
* @returns {Array} lexical tokens
|
||||
*/
|
||||
function lex(css) {
|
||||
var start; // Debug timer start.
|
||||
|
||||
var buffer = ''; // Character accumulator
|
||||
var ch; // Current character
|
||||
var column = 0; // Current source column number
|
||||
var cursor = -1; // Current source cursor position
|
||||
var depth = 0; // Current nesting depth
|
||||
var line = 1; // Current source line number
|
||||
var state = 'before-selector'; // Current state
|
||||
var stack = [state]; // State stack
|
||||
var token = {}; // Current token
|
||||
var tokens = []; // Token accumulator
|
||||
|
||||
// Supported @-rules, in roughly descending order of usage probability.
|
||||
var atRules = [
|
||||
'media',
|
||||
'keyframes',
|
||||
{ name: '-webkit-keyframes', type: 'keyframes', prefix: '-webkit-' },
|
||||
{ name: '-moz-keyframes', type: 'keyframes', prefix: '-moz-' },
|
||||
{ name: '-ms-keyframes', type: 'keyframes', prefix: '-ms-' },
|
||||
{ name: '-o-keyframes', type: 'keyframes', prefix: '-o-' },
|
||||
'font-face',
|
||||
{ name: 'import', state: 'before-at-value' },
|
||||
{ name: 'charset', state: 'before-at-value' },
|
||||
'supports',
|
||||
'viewport',
|
||||
{ name: 'namespace', state: 'before-at-value' },
|
||||
'document',
|
||||
{ name: '-moz-document', type: 'document', prefix: '-moz-' },
|
||||
'page'
|
||||
];
|
||||
|
||||
// -- Functions ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Advance the character cursor and return the next character.
|
||||
*
|
||||
* @returns {String} The next character.
|
||||
*/
|
||||
function getCh() {
|
||||
skip();
|
||||
return css[cursor];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the state at the given index in the stack.
|
||||
* The stack is LIFO so indexing is from the right.
|
||||
*
|
||||
* @param {Number} [index=0] Index to return.
|
||||
* @returns {String} state
|
||||
*/
|
||||
function getState(index) {
|
||||
return index ? stack[stack.length - 1 - index] : state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look ahead for a string beginning from the next position. The string
|
||||
* being looked for must start at the next position.
|
||||
*
|
||||
* @param {String} str The string to look for.
|
||||
* @returns {Boolean} Whether the string was found.
|
||||
*/
|
||||
function isNextString(str) {
|
||||
var start = cursor + 1;
|
||||
return (str === css.slice(start, start + str.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the start position of a substring beginning from the next
|
||||
* position. The string being looked for may begin anywhere.
|
||||
*
|
||||
* @param {String} str The substring to look for.
|
||||
* @returns {Number|false} The position, or `false` if not found.
|
||||
*/
|
||||
function find(str) {
|
||||
var pos = css.slice(cursor).indexOf(str);
|
||||
|
||||
return pos > 0 ? pos : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a character is next.
|
||||
*
|
||||
* @param {String} ch Character.
|
||||
* @returns {Boolean} Whether the character is next.
|
||||
*/
|
||||
function isNextChar(ch) {
|
||||
return ch === peek(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the character at the given cursor offset. The offset is relative
|
||||
* to the cursor, so negative values move backwards.
|
||||
*
|
||||
* @param {Number} [offset=1] Cursor offset.
|
||||
* @returns {String} Character.
|
||||
*/
|
||||
function peek(offset) {
|
||||
return css[cursor + (offset || 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the current state from the stack and set the new current state.
|
||||
*
|
||||
* @returns {String} The removed state.
|
||||
*/
|
||||
function popState() {
|
||||
var removed = stack.pop();
|
||||
state = stack[stack.length - 1];
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current state and add it to the stack.
|
||||
*
|
||||
* @param {String} newState The new state.
|
||||
* @returns {Number} The new stack length.
|
||||
*/
|
||||
function pushState(newState) {
|
||||
state = newState;
|
||||
stack.push(state);
|
||||
|
||||
return stack.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the current state with a new state.
|
||||
*
|
||||
* @param {String} newState The new state.
|
||||
* @returns {String} The replaced state.
|
||||
*/
|
||||
function replaceState(newState) {
|
||||
var previousState = state;
|
||||
stack[stack.length - 1] = state = newState;
|
||||
|
||||
return previousState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the character cursor. Positive numbers move the cursor forward.
|
||||
* Negative numbers are not supported!
|
||||
*
|
||||
* @param {Number} [n=1] Number of characters to skip.
|
||||
*/
|
||||
function skip(n) {
|
||||
if ((n || 1) == 1) {
|
||||
if (css[cursor] == '\n') {
|
||||
line++;
|
||||
column = 1;
|
||||
} else {
|
||||
column++;
|
||||
}
|
||||
cursor++;
|
||||
} else {
|
||||
var skipStr = css.slice(cursor, cursor + n).split('\n');
|
||||
if (skipStr.length > 1) {
|
||||
line += skipStr.length - 1;
|
||||
column = 1;
|
||||
}
|
||||
column += skipStr[skipStr.length - 1].length;
|
||||
cursor = cursor + n;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the current token to the pile and reset the buffer.
|
||||
*/
|
||||
function addToken() {
|
||||
token.end = {
|
||||
line: line,
|
||||
col: column
|
||||
};
|
||||
|
||||
DEBUG && debug('addToken:', JSON.stringify(token, null, 2));
|
||||
|
||||
tokens.push(token);
|
||||
|
||||
buffer = '';
|
||||
token = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current token.
|
||||
*
|
||||
* @param {String} type Token type.
|
||||
*/
|
||||
function initializeToken(type) {
|
||||
token = {
|
||||
type: type,
|
||||
start: {
|
||||
line: line,
|
||||
col : column
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// -- Main Loop ------------------------------------------------------------
|
||||
|
||||
/*
|
||||
The main loop is a state machine that reads in one character at a time,
|
||||
and determines what to do based on the current state and character.
|
||||
This is implemented as a series of nested `switch` statements and the
|
||||
case orders have been mildly optimized based on rough probabilities
|
||||
calculated by processing a small sample of real-world CSS.
|
||||
|
||||
Further optimization (such as a dispatch table) shouldn't be necessary
|
||||
since the total number of cases is very low.
|
||||
*/
|
||||
|
||||
TIMER && (start = Date.now());
|
||||
|
||||
while (ch = getCh()) {
|
||||
DEBUG && debug(ch, getState());
|
||||
|
||||
// column += 1;
|
||||
|
||||
switch (ch) {
|
||||
// Space
|
||||
case ' ':
|
||||
switch (getState()) {
|
||||
case 'selector':
|
||||
case 'value':
|
||||
case 'value-paren':
|
||||
case 'at-group':
|
||||
case 'at-value':
|
||||
case 'comment':
|
||||
case 'double-string':
|
||||
case 'single-string':
|
||||
buffer += ch;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
// Newline or tab
|
||||
case '\n':
|
||||
case '\t':
|
||||
case '\r':
|
||||
case '\f':
|
||||
switch (getState()) {
|
||||
case 'value':
|
||||
case 'value-paren':
|
||||
case 'at-group':
|
||||
case 'comment':
|
||||
case 'single-string':
|
||||
case 'double-string':
|
||||
case 'selector':
|
||||
buffer += ch;
|
||||
break;
|
||||
|
||||
case 'at-value':
|
||||
// Tokenize an @-rule if a semi-colon was omitted.
|
||||
if ('\n' === ch) {
|
||||
token.value = buffer.trim();
|
||||
addToken();
|
||||
popState();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// if ('\n' === ch) {
|
||||
// column = 0;
|
||||
// line += 1;
|
||||
// }
|
||||
break;
|
||||
|
||||
case ':':
|
||||
switch (getState()) {
|
||||
case 'name':
|
||||
token.name = buffer.trim();
|
||||
buffer = '';
|
||||
|
||||
replaceState('before-value');
|
||||
break;
|
||||
|
||||
case 'before-selector':
|
||||
buffer += ch;
|
||||
|
||||
initializeToken('selector');
|
||||
pushState('selector');
|
||||
break;
|
||||
|
||||
case 'before-value':
|
||||
replaceState('value');
|
||||
buffer += ch;
|
||||
break;
|
||||
|
||||
default:
|
||||
buffer += ch;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case ';':
|
||||
switch (getState()) {
|
||||
case 'name':
|
||||
case 'before-value':
|
||||
case 'value':
|
||||
// Tokenize a declaration
|
||||
// if value is empty skip the declaration
|
||||
if (buffer.trim().length > 0) {
|
||||
token.value = buffer.trim(),
|
||||
addToken();
|
||||
}
|
||||
replaceState('before-name');
|
||||
break;
|
||||
|
||||
case 'value-paren':
|
||||
// Insignificant semi-colon
|
||||
buffer += ch;
|
||||
break;
|
||||
|
||||
case 'at-value':
|
||||
// Tokenize an @-rule
|
||||
token.value = buffer.trim();
|
||||
addToken();
|
||||
popState();
|
||||
break;
|
||||
|
||||
case 'before-name':
|
||||
// Extraneous semi-colon
|
||||
break;
|
||||
|
||||
default:
|
||||
buffer += ch;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case '{':
|
||||
switch (getState()) {
|
||||
case 'selector':
|
||||
// If the sequence is `\{` then assume that the brace should be escaped.
|
||||
if (peek(-1) === '\\') {
|
||||
buffer += ch;
|
||||
break;
|
||||
}
|
||||
|
||||
// Tokenize a selector
|
||||
token.text = buffer.trim();
|
||||
addToken();
|
||||
replaceState('before-name');
|
||||
depth = depth + 1;
|
||||
break;
|
||||
|
||||
case 'at-group':
|
||||
// Tokenize an @-group
|
||||
token.name = buffer.trim();
|
||||
|
||||
// XXX: @-rules are starting to get hairy
|
||||
switch (token.type) {
|
||||
case 'font-face':
|
||||
case 'viewport' :
|
||||
case 'page' :
|
||||
pushState('before-name');
|
||||
break;
|
||||
|
||||
default:
|
||||
pushState('before-selector');
|
||||
}
|
||||
|
||||
addToken();
|
||||
depth = depth + 1;
|
||||
break;
|
||||
|
||||
case 'name':
|
||||
case 'at-rule':
|
||||
// Tokenize a declaration or an @-rule
|
||||
token.name = buffer.trim();
|
||||
addToken();
|
||||
pushState('before-name');
|
||||
depth = depth + 1;
|
||||
break;
|
||||
|
||||
case 'comment':
|
||||
case 'double-string':
|
||||
case 'single-string':
|
||||
// Ignore braces in comments and strings
|
||||
buffer += ch;
|
||||
break;
|
||||
case 'before-value':
|
||||
replaceState('value');
|
||||
buffer += ch;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case '}':
|
||||
switch (getState()) {
|
||||
case 'before-name':
|
||||
case 'name':
|
||||
case 'before-value':
|
||||
case 'value':
|
||||
// If the buffer contains anything, it is a value
|
||||
if (buffer) {
|
||||
token.value = buffer.trim();
|
||||
}
|
||||
|
||||
// If the current token has a name and a value it should be tokenized.
|
||||
if (token.name && token.value) {
|
||||
addToken();
|
||||
}
|
||||
|
||||
// Leave the block
|
||||
initializeToken('end');
|
||||
addToken();
|
||||
popState();
|
||||
|
||||
// We might need to leave again.
|
||||
// XXX: What about 3 levels deep?
|
||||
if ('at-group' === getState()) {
|
||||
initializeToken('at-group-end');
|
||||
addToken();
|
||||
popState();
|
||||
}
|
||||
|
||||
if (depth > 0) {
|
||||
depth = depth - 1;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'at-group':
|
||||
case 'before-selector':
|
||||
case 'selector':
|
||||
// If the sequence is `\}` then assume that the brace should be escaped.
|
||||
if (peek(-1) === '\\') {
|
||||
buffer += ch;
|
||||
break;
|
||||
}
|
||||
|
||||
if (depth > 0) {
|
||||
// Leave block if in an at-group
|
||||
if ('at-group' === getState(1)) {
|
||||
initializeToken('at-group-end');
|
||||
addToken();
|
||||
}
|
||||
}
|
||||
|
||||
if (depth > 1) {
|
||||
popState();
|
||||
}
|
||||
|
||||
if (depth > 0) {
|
||||
depth = depth - 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'double-string':
|
||||
case 'single-string':
|
||||
case 'comment':
|
||||
// Ignore braces in comments and strings.
|
||||
buffer += ch;
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Strings
|
||||
case '"':
|
||||
case "'":
|
||||
switch (getState()) {
|
||||
case 'double-string':
|
||||
if ('"' === ch && '\\' !== peek(-1)) {
|
||||
popState();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'single-string':
|
||||
if ("'" === ch && '\\' !== peek(-1)) {
|
||||
popState();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'before-at-value':
|
||||
replaceState('at-value');
|
||||
pushState('"' === ch ? 'double-string' : 'single-string');
|
||||
break;
|
||||
|
||||
case 'before-value':
|
||||
replaceState('value');
|
||||
pushState('"' === ch ? 'double-string' : 'single-string');
|
||||
break;
|
||||
|
||||
case 'comment':
|
||||
// Ignore strings within comments.
|
||||
break;
|
||||
|
||||
default:
|
||||
if ('\\' !== peek(-1)) {
|
||||
pushState('"' === ch ? 'double-string' : 'single-string');
|
||||
}
|
||||
}
|
||||
|
||||
buffer += ch;
|
||||
break;
|
||||
|
||||
// Comments
|
||||
case '/':
|
||||
switch (getState()) {
|
||||
case 'comment':
|
||||
case 'double-string':
|
||||
case 'single-string':
|
||||
// Ignore
|
||||
buffer += ch;
|
||||
break;
|
||||
|
||||
case 'before-value':
|
||||
case 'selector':
|
||||
case 'name':
|
||||
case 'value':
|
||||
if (isNextChar('*')) {
|
||||
// Ignore comments in selectors, properties and values. They are
|
||||
// difficult to represent in the AST.
|
||||
var pos = find('*/');
|
||||
|
||||
if (pos) {
|
||||
skip(pos + 1);
|
||||
}
|
||||
} else {
|
||||
if (getState() == 'before-value') replaceState('value');
|
||||
buffer += ch;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (isNextChar('*')) {
|
||||
// Create a comment token
|
||||
initializeToken('comment');
|
||||
pushState('comment');
|
||||
skip();
|
||||
}
|
||||
else {
|
||||
buffer += ch;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
// Comment end or universal selector
|
||||
case '*':
|
||||
switch (getState()) {
|
||||
case 'comment':
|
||||
if (isNextChar('/')) {
|
||||
// Tokenize a comment
|
||||
token.text = buffer; // Don't trim()!
|
||||
skip();
|
||||
addToken();
|
||||
popState();
|
||||
}
|
||||
else {
|
||||
buffer += ch;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'before-selector':
|
||||
buffer += ch;
|
||||
initializeToken('selector');
|
||||
pushState('selector');
|
||||
break;
|
||||
|
||||
case 'before-value':
|
||||
replaceState('value');
|
||||
buffer += ch;
|
||||
break;
|
||||
|
||||
default:
|
||||
buffer += ch;
|
||||
}
|
||||
break;
|
||||
|
||||
// @-rules
|
||||
case '@':
|
||||
switch (getState()) {
|
||||
case 'comment':
|
||||
case 'double-string':
|
||||
case 'single-string':
|
||||
buffer += ch;
|
||||
break;
|
||||
case 'before-value':
|
||||
replaceState('value');
|
||||
buffer += ch;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Iterate over the supported @-rules and attempt to tokenize one.
|
||||
var tokenized = false;
|
||||
var name;
|
||||
var rule;
|
||||
|
||||
for (var j = 0, len = atRules.length; !tokenized && j < len; ++j) {
|
||||
rule = atRules[j];
|
||||
name = rule.name || rule;
|
||||
|
||||
if (!isNextString(name)) { continue; }
|
||||
|
||||
tokenized = true;
|
||||
|
||||
initializeToken(name);
|
||||
pushState(rule.state || 'at-group');
|
||||
skip(name.length);
|
||||
|
||||
if (rule.prefix) {
|
||||
token.prefix = rule.prefix;
|
||||
}
|
||||
|
||||
if (rule.type) {
|
||||
token.type = rule.type;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tokenized) {
|
||||
// Keep on truckin' America!
|
||||
buffer += ch;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
// Parentheses are tracked to disambiguate semi-colons, such as within a
|
||||
// data URI.
|
||||
case '(':
|
||||
switch (getState()) {
|
||||
case 'value':
|
||||
pushState('value-paren');
|
||||
break;
|
||||
case 'before-value':
|
||||
replaceState('value');
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += ch;
|
||||
break;
|
||||
|
||||
case ')':
|
||||
switch (getState()) {
|
||||
case 'value-paren':
|
||||
popState();
|
||||
break;
|
||||
case 'before-value':
|
||||
replaceState('value');
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += ch;
|
||||
break;
|
||||
|
||||
default:
|
||||
switch (getState()) {
|
||||
case 'before-selector':
|
||||
initializeToken('selector');
|
||||
pushState('selector');
|
||||
break;
|
||||
|
||||
case 'before-name':
|
||||
initializeToken('property');
|
||||
replaceState('name');
|
||||
break;
|
||||
|
||||
case 'before-value':
|
||||
replaceState('value');
|
||||
break;
|
||||
|
||||
case 'before-at-value':
|
||||
replaceState('at-value');
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += ch;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
TIMER && debug('ran in', (Date.now() - start) + 'ms');
|
||||
|
||||
return tokens;
|
||||
}
|
291
book/node_modules/mensch/lib/parser.js
generated
vendored
Normal file
291
book/node_modules/mensch/lib/parser.js
generated
vendored
Normal file
@ -0,0 +1,291 @@
|
||||
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; });
|
||||
}
|
258
book/node_modules/mensch/lib/stringify.js
generated
vendored
Normal file
258
book/node_modules/mensch/lib/stringify.js
generated
vendored
Normal file
@ -0,0 +1,258 @@
|
||||
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 + ';';
|
||||
}
|
Reference in New Issue
Block a user