install gulp

This commit is contained in:
2018-05-28 13:26:25 +02:00
parent 2de92c908b
commit 04fab6ba46
8032 changed files with 709336 additions and 3100 deletions
+129
View File
@@ -0,0 +1,129 @@
var TokenType = require('../token-types');
/**
* Mark whitespaces and comments
* @param {Array} tokens
*/
function markSpacesAndComments(tokens) {
var tokensLength = tokens.length;
var lastSpaceIndex = -1;
var lastSpaceOrCommentIndex = -1;
var spaces = [-1, -1];
var token; // current token
var type; // current token's type
// For every token in the token list, mark spaces and line breaks
// as spaces (set both `ws` and `sc` flags). Mark multiline comments
// with `sc` flag.
// If there are several spaces or tabs or line breaks or multiline
// comments in a row, group them: take the last one's index number
// and save it to the first token in the group as a reference
// (e.g., `ws_last = 7` for a group of whitespaces or `sc_last = 9`
// for a group of whitespaces and comments):
for (var i = 0; i < tokensLength; i++) {
type = tokens[i].type;
if (type === TokenType.Space ||
type === TokenType.Tab ||
type === TokenType.Newline) {
markSpace(tokens, i, spaces);
} else if (type === TokenType.CommentML) {
markComment(tokens, i, spaces);
} else {
markEndOfSpacesAndComments(tokens, i, spaces);
}
}
markEndOfSpacesAndComments(tokens, i, spaces);
}
function markSpace(tokens, i, spaces) {
var token = tokens[i];
token.ws = true;
token.sc = true;
if (spaces[0] === -1) spaces[0] = i;
if (spaces[1] === -1) spaces[1] = i;
}
function markComment(tokens, i, spaces) {
var ws = spaces[0];
tokens[i].sc = true;
if (ws !== -1) {
tokens[ws].ws_last = i - 1;
spaces[0] = -1;
}
}
function markEndOfSpacesAndComments(tokens, i, spaces) {
var ws = spaces[0];
var sc = spaces[1];
if (ws !== -1) {
tokens[ws].ws_last = i - 1;
spaces[0] = -1;
}
if (sc !== -1) {
tokens[sc].sc_last = i - 1;
spaces[1] = -1;
}
}
/**
* Pair brackets
* @param {Array} tokens
*/
function markBrackets(tokens) {
var tokensLength = tokens.length;
var ps = [], // parenthesis
sbs = [], // square brackets
cbs = [], // curly brackets
t; // current token
// For every token in the token list, if we meet an opening (left)
// bracket, push its index number to a corresponding array.
// If we then meet a closing (right) bracket, look at the corresponding
// array. If there are any elements (records about previously met
// left brackets), take a token of the last left bracket (take
// the last index number from the array and find a token with
// this index number) and save right bracket's index as a reference:
for (var i = 0; i < tokens.length; i++) {
t = tokens[i];
var type = t.type;
if (type === TokenType.LeftParenthesis) {
ps.push(i);
} else if (type === TokenType.RightParenthesis) {
if (ps.length) {
t.left = ps.pop();
tokens[t.left].right = i;
}
} else if (type === TokenType.LeftSquareBracket) {
sbs.push(i);
} else if (type === TokenType.RightSquareBracket) {
if (sbs.length) {
t.left = sbs.pop();
tokens[t.left].right = i;
}
} else if (type === TokenType.LeftCurlyBracket) {
cbs.push(i);
} else if (type === TokenType.RightCurlyBracket) {
if (cbs.length) {
t.left = cbs.pop();
tokens[t.left].right = i;
}
}
}
}
/**
* @param {Array} tokens
*/
function markTokens(tokens) {
// Mark paired brackets:
markBrackets(tokens);
// Mark whitespaces and comments:
markSpacesAndComments(tokens);
}
module.exports = markTokens;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
module.exports = function stringify(tree) {
// TODO: Better error message
if (!tree) throw new Error('We need tree to translate');
function _t(tree) {
var type = tree.type;
if (_unique[type]) return _unique[type](tree);
if (typeof tree.content === 'string') return tree.content;
if (Array.isArray(tree.content)) return _composite(tree.content);
return '';
}
function _composite(t, i) {
if (!t) return '';
var s = '';
i = i || 0;
for (; i < t.length; i++) s += _t(t[i]);
return s;
}
var _unique = {
'arguments': function(t) {
return '(' + _composite(t.content) + ')';
},
'atkeyword': function(t) {
return '@' + _composite(t.content);
},
'atruler': function(t) {
return _t(t.content[0]) + _t(t.content[1]) + '{' + _t(t.content[2]) + '}';
},
'attribute': function(t) {
return '[' + _composite(t.content) + ']';
},
'block': function(t) {
return '{' + _composite(t.content) + '}';
},
'braces': function(t) {
return t.content[0] + _composite(t.content.slice(2)) + t.content[1];
},
'class': function(t) {
return '.' + _composite(t.content);
},
'color': function(t) {
return '#' + t.content;
},
'filter': function(t) {
return _t(t.content[0]) + ':' + _t(t.content[1]);
},
'functionExpression': function(t) {
return 'expression(' + t.content + ')';
},
'id': function (t) {
return '#' + t.content;
},
'important': function(t) {
return '!' + _composite(t.content) + 'important';
},
'multilineComment': function(t) {
return '/*' + t.content + '*/';
},
'nthSelector': function(t) {
return ':' + _t(t.content[0]) + '(' + _composite(t.content.slice(1)) + ')';
},
'percentage': function(t) {
return _composite(t.content) + '%';
},
'pseudoClass': function(t) {
return ':' + _composite(t.content);
},
'pseudoElement': function(t) {
return '::' + _composite(t.content);
},
'uri': function(t) {
return 'url(' + _composite(t.content) + ')';
}
};
return _t(tree);
}
@@ -0,0 +1,273 @@
module.exports = function(css) {
var TokenType = require('../token-types');
var tokens = [],
urlMode = false,
blockMode = 0,
pos = 0,
tn = 0,
ln = 1,
col = 1;
var cssLength = 0;
var Punctuation = {
' ': TokenType.Space,
'\n': TokenType.Newline,
'\r': TokenType.Newline,
'\t': TokenType.Tab,
'!': TokenType.ExclamationMark,
'"': TokenType.QuotationMark,
'#': TokenType.NumberSign,
'$': TokenType.DollarSign,
'%': TokenType.PercentSign,
'&': TokenType.Ampersand,
'\'': TokenType.Apostrophe,
'(': TokenType.LeftParenthesis,
')': TokenType.RightParenthesis,
'*': TokenType.Asterisk,
'+': TokenType.PlusSign,
',': TokenType.Comma,
'-': TokenType.HyphenMinus,
'.': TokenType.FullStop,
'/': TokenType.Solidus,
':': TokenType.Colon,
';': TokenType.Semicolon,
'<': TokenType.LessThanSign,
'=': TokenType.EqualsSign,
'>': TokenType.GreaterThanSign,
'?': TokenType.QuestionMark,
'@': TokenType.CommercialAt,
'[': TokenType.LeftSquareBracket,
']': TokenType.RightSquareBracket,
'^': TokenType.CircumflexAccent,
'_': TokenType.LowLine,
'{': TokenType.LeftCurlyBracket,
'|': TokenType.VerticalLine,
'}': TokenType.RightCurlyBracket,
'~': TokenType.Tilde
};
/**
* Add a token to the token list
* @param {string} type
* @param {string} value
*/
function pushToken(type, value, column) {
tokens.push({
tn: tn++,
ln: ln,
col: column,
type: type,
value: value
});
}
/**
* Check if a character is a decimal digit
* @param {string} c Character
* @returns {boolean}
*/
function isDecimalDigit(c) {
return '0123456789'.indexOf(c) >= 0;
}
/**
* Parse spaces
* @param {string} css Unparsed part of CSS string
*/
function parseSpaces(css) {
var start = pos;
// Read the string until we meet a non-space character:
for (; pos < cssLength; pos++) {
if (css.charAt(pos) !== ' ') break;
}
// Add a substring containing only spaces to tokens:
pushToken(TokenType.Space, css.substring(start, pos--), col);
col += (pos - start);
}
/**
* Parse a string within quotes
* @param {string} css Unparsed part of CSS string
* @param {string} q Quote (either `'` or `"`)
*/
function parseString(css, q) {
var start = pos;
// Read the string until we meet a matching quote:
for (pos++; pos < cssLength; pos++) {
// Skip escaped quotes:
if (css.charAt(pos) === '\\') pos++;
else if (css.charAt(pos) === q) break;
}
// Add the string (including quotes) to tokens:
pushToken(q === '"' ? TokenType.StringDQ : TokenType.StringSQ, css.substring(start, pos + 1), col);
col += (pos - start);
}
/**
* Parse numbers
* @param {string} css Unparsed part of CSS string
*/
function parseDecimalNumber(css) {
var start = pos;
// Read the string until we meet a character that's not a digit:
for (; pos < cssLength; pos++) {
if (!isDecimalDigit(css.charAt(pos))) break;
}
// Add the number to tokens:
pushToken(TokenType.DecimalNumber, css.substring(start, pos--), col);
col += (pos - start);
}
/**
* Parse identifier
* @param {string} css Unparsed part of CSS string
*/
function parseIdentifier(css) {
var start = pos;
// Skip all opening slashes:
while (css.charAt(pos) === '/') pos++;
// Read the string until we meet a punctuation mark:
for (; pos < cssLength; pos++) {
// Skip all '\':
if (css.charAt(pos) === '\\') pos++;
else if (Punctuation[css.charAt(pos)]) break;
}
var ident = css.substring(start, pos--);
// Enter url mode if parsed substring is `url`:
urlMode = urlMode || ident === 'url';
// Add identifier to tokens:
pushToken(TokenType.Identifier, ident, col);
col += (pos - start);
}
/**
* Parse a multiline comment
* @param {string} css Unparsed part of CSS string
*/
function parseMLComment(css) {
var start = pos;
// Read the string until we meet `*/`.
// Since we already know first 2 characters (`/*`), start reading
// from `pos + 2`:
for (pos = pos + 2; pos < cssLength; pos++) {
if (css.charAt(pos) === '*' && css.charAt(pos + 1) === '/') {
pos++;
break;
}
}
// Add full comment (including `/*` and `*/`) to the list of tokens:
var comment = css.substring(start, pos + 1);
pushToken(TokenType.CommentML, comment, col);
var newlines = comment.split('\n');
if (newlines.length > 1) {
ln += newlines.length - 1;
col = newlines[newlines.length - 1].length;
} else {
col += (pos - start);
}
}
function parseSLComment(css) {
var start = pos;
// Read the string until we meet line break.
// Since we already know first 2 characters (`//`), start reading
// from `pos + 2`:
for (pos+=2; pos < cssLength; pos++) {
if (css.charAt(pos) === '\n' || css.charAt(pos) === '\r') {
break;
}
}
// Add comment (including `//` and line break) to the list of tokens:
pushToken(TokenType.CommentSL, css.substring(start, pos--), col);
col += pos - start;
}
/**
* Convert a CSS string to a list of tokens
* @param {string} css CSS string
* @returns {Array} List of tokens
* @private
*/
function getTokens(css) {
var c; // current character
var cn; // next character
cssLength = css.length;
// Parse string, character by character:
for (pos = 0; pos < cssLength; col++, pos++) {
c = css.charAt(pos);
cn = css.charAt(pos + 1);
// If we meet `/*`, it's a start of a multiline comment.
// Parse following characters as a multiline comment:
if (c === '/' && cn === '*') {
parseMLComment(css);
}
// If we meet `//` and it is not a part of url:
else if (!urlMode && c === '/' && cn === '/') {
// If we're currently inside a block, treat `//` as a start
// of identifier. Else treat `//` as a start of a single-line
// comment:
if (blockMode > 0) parseIdentifier(css);
else parseSLComment(css);
}
// If current character is a double or single quote, it's a start
// of a string:
else if (c === '"' || c === "'") {
parseString(css, c);
}
// If current character is a space:
else if (c === ' ') {
parseSpaces(css);
}
// If current character is a punctuation mark:
else if (Punctuation[c]) {
// Add it to the list of tokens:
pushToken(Punctuation[c], c, col);
if (c === '\n' || c === '\r') {
ln++;
col = 0;
} // Go to next line
else if (c === ')') urlMode = false; // exit url mode
else if (c === '{') blockMode++; // enter a block
else if (c === '}') blockMode--; // exit a block
}
// If current character is a decimal digit:
else if (isDecimalDigit(c)) {
parseDecimalNumber(css);
}
// If current character is anything else:
else {
parseIdentifier(css);
}
}
return tokens;
}
return getTokens(css);
};
+9
View File
@@ -0,0 +1,9 @@
var Node = require('./node');
var parse = require('./parse');
module.exports = {
createNode: function(options) {
return new Node(options.type, options.content);
},
parse: parse
}
+116
View File
@@ -0,0 +1,116 @@
var TokenType = require('../token-types');
module.exports = (function() {
/**
* Mark whitespaces and comments
*/
function markSC(tokens) {
var tokensLength = tokens.length,
ws = -1, // flag for whitespaces
sc = -1, // flag for whitespaces and comments
t; // current token
// For every token in the token list, mark spaces and line breaks
// as spaces (set both `ws` and `sc` flags). Mark multiline comments
// with `sc` flag.
// If there are several spaces or tabs or line breaks or multiline
// comments in a row, group them: take the last one's index number
// and save it to the first token in the group as a reference
// (e.g., `ws_last = 7` for a group of whitespaces or `sc_last = 9`
// for a group of whitespaces and comments):
for (var i = 0; i < tokensLength; i++) {
t = tokens[i];
switch (t.type) {
case TokenType.Space:
case TokenType.Tab:
case TokenType.Newline:
t.ws = true;
t.sc = true;
if (ws === -1) ws = i;
if (sc === -1) sc = i;
break;
case TokenType.CommentML:
case TokenType.CommentSL:
if (ws !== -1) {
tokens[ws].ws_last = i - 1;
ws = -1;
}
t.sc = true;
break;
default:
if (ws !== -1) {
tokens[ws].ws_last = i - 1;
ws = -1;
}
if (sc !== -1) {
tokens[sc].sc_last = i - 1;
sc = -1;
}
}
}
if (ws !== -1) tokens[ws].ws_last = i - 1;
if (sc !== -1) tokens[sc].sc_last = i - 1;
}
/**
* Pair brackets
*/
function markBrackets(tokens) {
var tokensLength = tokens.length;
var ps = [], // parenthesis
sbs = [], // square brackets
cbs = [], // curly brackets
t; // current token
// For every token in the token list, if we meet an opening (left)
// bracket, push its index number to a corresponding array.
// If we then meet a closing (right) bracket, look at the corresponding
// array. If there are any elements (records about previously met
// left brackets), take a token of the last left bracket (take
// the last index number from the array and find a token with
// this index number) and save right bracket's index as a reference:
for (var i = 0; i < tokens.length; i++) {
t = tokens[i];
switch(t.type) {
case TokenType.LeftParenthesis:
ps.push(i);
break;
case TokenType.RightParenthesis:
if (ps.length) {
t.left = ps.pop();
tokens[t.left].right = i;
}
break;
case TokenType.LeftSquareBracket:
sbs.push(i);
break;
case TokenType.RightSquareBracket:
if (sbs.length) {
t.left = sbs.pop();
tokens[t.left].right = i;
}
break;
case TokenType.LeftCurlyBracket:
cbs.push(i);
break;
case TokenType.RightCurlyBracket:
if (cbs.length) {
t.left = cbs.pop();
tokens[t.left].right = i;
}
break;
}
}
}
return function(tokens) {
markBrackets(tokens);
markSC(tokens);
}
})();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,95 @@
module.exports = function stringify(tree) {
// TODO: Better error message
if (!tree) throw new Error('We need tree to translate');
function _t(tree) {
var type = tree.type;
if (_unique[type]) return _unique[type](tree);
if (typeof tree.content === 'string') return tree.content;
if (Array.isArray(tree.content)) return _composite(tree.content);
return '';
}
function _composite(t, i) {
if (!t) return '';
var s = '';
i = i || 0;
for (; i < t.length; i++) s += _t(t[i]);
return s;
}
var _unique = {
'arguments': function(t) {
return '(' + _composite(t.content) + ')';
},
'atkeyword': function(t) {
return '@' + _composite(t.content);
},
'atruler': function(t) {
return _t(t.content[0]) + _t(t.content[1]) + '{' + _t(t.content[2]) + '}';
},
'attribute': function(t) {
return '[' + _composite(t.content) + ']';
},
'block': function(t) {
return '{' + _composite(t.content) + '}';
},
'braces': function(t) {
return t.content[0] + _composite(t.content.slice(2)) + t.content[1];
},
'class': function(t) {
return '.' + _composite(t.content);
},
'color': function(t) {
return '#' + t.content;
},
'escapedString': function(t) {
return '~' + t.content;
},
'filter': function(t) {
return _t(t.content[0]) + ':' + _t(t.content[1]);
},
'functionExpression': function(t) {
return 'expression(' + t.content + ')';
},
'id': function (t) {
return '#' + t.content;
},
'important': function(t) {
return '!' + _composite(t.content) + 'important';
},
'interpolatedVariable': function(t) {
return '@{' + _composite(t.content) + '}';
},
'multilineComment': function (t) {
return '/*' + t.content + '*/';
},
'nthSelector': function(t) {
return ':' + _t(t.content[0]) + '(' + _composite(t.content.slice(1)) + ')';
},
'percentage': function(t) {
return _t(t.content) + '%';
},
'pseudoClass': function(t) {
return ':' + _composite(t.content);
},
'pseudoElement': function(t) {
return '::' + _composite(t.content);
},
'singlelineComment': function (t) {
return '/' + '/' + t.content;
},
'uri': function(t) {
return 'url(' + _composite(t.content) + ')';
},
'variable': function(t) {
return '@' + _composite(t.content);
},
'variablesList': function(t) {
return _composite(t.content) + '...';
}
};
return _t(tree);
}
@@ -0,0 +1,272 @@
module.exports = function(css) {
var TokenType = require('../token-types');
var tokens = [],
urlMode = false,
blockMode = 0,
c, // current character
cn, // next character
pos = 0,
tn = 0,
ln = 1,
col = 1;
var Punctuation = {
' ': TokenType.Space,
'\n': TokenType.Newline,
'\r': TokenType.Newline,
'\t': TokenType.Tab,
'!': TokenType.ExclamationMark,
'"': TokenType.QuotationMark,
'#': TokenType.NumberSign,
'$': TokenType.DollarSign,
'%': TokenType.PercentSign,
'&': TokenType.Ampersand,
'\'': TokenType.Apostrophe,
'(': TokenType.LeftParenthesis,
')': TokenType.RightParenthesis,
'*': TokenType.Asterisk,
'+': TokenType.PlusSign,
',': TokenType.Comma,
'-': TokenType.HyphenMinus,
'.': TokenType.FullStop,
'/': TokenType.Solidus,
':': TokenType.Colon,
';': TokenType.Semicolon,
'<': TokenType.LessThanSign,
'=': TokenType.EqualsSign,
'>': TokenType.GreaterThanSign,
'?': TokenType.QuestionMark,
'@': TokenType.CommercialAt,
'[': TokenType.LeftSquareBracket,
']': TokenType.RightSquareBracket,
'^': TokenType.CircumflexAccent,
'_': TokenType.LowLine,
'{': TokenType.LeftCurlyBracket,
'|': TokenType.VerticalLine,
'}': TokenType.RightCurlyBracket,
'~': TokenType.Tilde
};
/**
* Add a token to the token list
* @param {string} type
* @param {string} value
*/
function pushToken(type, value, column) {
tokens.push({
tn: tn++,
ln: ln,
col: column,
type: type,
value: value
});
}
/**
* Check if a character is a decimal digit
* @param {string} c Character
* @returns {boolean}
*/
function isDecimalDigit(c) {
return '0123456789'.indexOf(c) >= 0;
}
/**
* Parse spaces
* @param {string} css Unparsed part of CSS string
*/
function parseSpaces(css) {
var start = pos;
// Read the string until we meet a non-space character:
for (; pos < css.length; pos++) {
if (css.charAt(pos) !== ' ') break;
}
// Add a substring containing only spaces to tokens:
pushToken(TokenType.Space, css.substring(start, pos--), col);
col += (pos - start);
}
/**
* Parse a string within quotes
* @param {string} css Unparsed part of CSS string
* @param {string} q Quote (either `'` or `"`)
*/
function parseString(css, q) {
var start = pos;
// Read the string until we meet a matching quote:
for (pos++; pos < css.length; pos++) {
// Skip escaped quotes:
if (css.charAt(pos) === '\\') pos++;
else if (css.charAt(pos) === q) break;
}
// Add the string (including quotes) to tokens:
pushToken(q === '"' ? TokenType.StringDQ : TokenType.StringSQ, css.substring(start, pos + 1), col);
col += (pos - start);
}
/**
* Parse numbers
* @param {string} css Unparsed part of CSS string
*/
function parseDecimalNumber(css) {
var start = pos;
// Read the string until we meet a character that's not a digit:
for (; pos < css.length; pos++) {
if (!isDecimalDigit(css.charAt(pos))) break;
}
// Add the number to tokens:
pushToken(TokenType.DecimalNumber, css.substring(start, pos--), col);
col += (pos - start);
}
/**
* Parse identifier
* @param {string} css Unparsed part of CSS string
*/
function parseIdentifier(css) {
var start = pos;
// Skip all opening slashes:
while (css.charAt(pos) === '/') pos++;
// Read the string until we meet a punctuation mark:
for (; pos < css.length; pos++) {
// Skip all '\':
if (css.charAt(pos) === '\\') pos++;
else if (css.charAt(pos) in Punctuation) break;
}
var ident = css.substring(start, pos--);
// Enter url mode if parsed substring is `url`:
urlMode = urlMode || ident === 'url';
// Add identifier to tokens:
pushToken(TokenType.Identifier, ident, col);
col += (pos - start);
}
/**
* Parse a multiline comment
* @param {string} css Unparsed part of CSS string
*/
function parseMLComment(css) {
var start = pos;
// Read the string until we meet `*/`.
// Since we already know first 2 characters (`/*`), start reading
// from `pos + 2`:
for (pos = pos + 2; pos < css.length; pos++) {
if (css.charAt(pos) === '*' && css.charAt(pos + 1) === '/') {
pos++;
break;
}
}
// Add full comment (including `/*` and `*/`) to the list of tokens:
var comment = css.substring(start, pos + 1);
pushToken(TokenType.CommentML, comment, col);
var newlines = comment.split('\n');
if (newlines.length > 1) {
ln += newlines.length - 1;
col = newlines[newlines.length - 1].length;
} else {
col += (pos - start);
}
}
/**
* Parse a single line comment
* @param {string} css Unparsed part of CSS string
*/
function parseSLComment(css) {
var start = pos;
// Read the string until we meet line break.
// Since we already know first 2 characters (`//`), start reading
// from `pos + 2`:
for (pos+=2; pos < css.length; pos++) {
if (css.charAt(pos) === '\n' || css.charAt(pos) === '\r') {
break;
}
}
// Add comment (including `//` and line break) to the list of tokens:
pushToken(TokenType.CommentSL, css.substring(start, pos--), col);
col += pos - start;
}
/**
* Convert a CSS string to a list of tokens
* @param {string} css CSS string
* @returns {Array} List of tokens
* @private
*/
function getTokens(css) {
// Parse string, character by character:
for (pos = 0; pos < css.length; col++, pos++) {
c = css.charAt(pos);
cn = css.charAt(pos + 1);
// If we meet `/*`, it's a start of a multiline comment.
// Parse following characters as a multiline comment:
if (c === '/' && cn === '*') {
parseMLComment(css);
}
// If we meet `//` and it is not a part of url:
else if (!urlMode && c === '/' && cn === '/') {
// If we're currently inside a block, treat `//` as a start
// of identifier. Else treat `//` as a start of a single-line
// comment:
parseSLComment(css);
}
// If current character is a double or single quote, it's a start
// of a string:
else if (c === '"' || c === "'") {
parseString(css, c);
}
// If current character is a space:
else if (c === ' ') {
parseSpaces(css);
}
// If current character is a punctuation mark:
else if (c in Punctuation) {
// Add it to the list of tokens:
pushToken(Punctuation[c], c, col);
if (c === '\n' || c === '\r') {
ln++;
col = 0;
} // Go to next line
if (c === ')') urlMode = false; // exit url mode
if (c === '{') blockMode++; // enter a block
if (c === '}') blockMode--; // exit a block
}
// If current character is a decimal digit:
else if (isDecimalDigit(c)) {
parseDecimalNumber(css);
}
// If current character is anything else:
else {
parseIdentifier(css);
}
}
return tokens;
}
return getTokens(css);
};
+63
View File
@@ -0,0 +1,63 @@
module.exports = {
ArgumentsType: 'arguments',
AtkeywordType: 'atkeyword',
AtrulebType: 'atruleb',
AtrulerType: 'atruler',
AtrulerqType: 'atrulerq',
AtrulersType: 'atrulers',
AtrulesType: 'atrules',
AttribType: 'attribute',
AttrselectorType: 'attributeSelector',
BlockType: 'block',
BracesType: 'braces',
ClassType: 'class',
CombinatorType: 'combinator',
CommentMLType: 'multilineComment',
CommentSLType: 'singlelineComment',
ConditionType: 'condition',
ConditionalStatementType: 'conditionalStatement',
DeclarationType: 'declaration',
DeclDelimType: 'declarationDelimiter',
DefaultType: 'default',
DelimType: 'delimiter',
DimensionType: 'dimension',
EscapedStringType: 'escapedString',
FilterType: 'filter',
FiltervType: 'filterValue',
FunctionType: 'function',
FunctionExpressionType: 'functionExpression',
IdentType: 'ident',
ImportantType: 'important',
IncludeType: 'include',
InterpolationType: 'interpolation',
InterpolatedVariableType: 'interpolatedVariable',
LoopType: 'loop',
MixinType: 'mixin',
NamespaceType: 'namespace',
NthType: 'nth',
NthselectorType: 'nthSelector',
NumberType: 'number',
OperatorType: 'operator',
ParentSelectorType: 'parentSelector',
PercentageType: 'percentage',
PlaceholderType: 'placeholder',
ProgidType: 'progid',
PropertyType: 'property',
PropertyDelimType: 'propertyDelimiter',
PseudocType: 'pseudoClass',
PseudoeType: 'pseudoElement',
RawType: 'raw',
RulesetType: 'ruleset',
SType: 'space',
SelectorType: 'selector',
ShashType: 'id',
SimpleselectorType: 'simpleSelector',
StringType: 'string',
StylesheetType: 'stylesheet',
UnaryType: 'unaryOperator',
UriType: 'uri',
ValueType: 'value',
VariableType: 'variable',
VariablesListType: 'variablesList',
VhashType: 'color'
};
+151
View File
@@ -0,0 +1,151 @@
/**
* @param {string} type
* @param {array|string} content
* @param {number} line
* @param {number} column
* @constructor
*/
function Node(type, content, line, column) {
this.type = type;
this.content = content;
this.start = {
line: line,
column: column
};
}
Node.prototype = {
type: null,
content: null,
start: null,
/**
* @param {String} type Node type
* @return {Boolean} Whether there is a child node of given type
*/
contains: function(type) {
return this.content.some(function(node) {
return node.type === type;
});
},
/**
* @param {String} type
* @return {Node} First child node
*/
first: function(type) {
if (!type || !Array.isArray(this.content)) return this.content[0];
var i = 0;
var l = this.content.length;
for (; i < l; i++) {
if (this.content[i].type === type) return this.content[i];
}
},
/**
* @param {String} type Node type
* @param {Function} callback Function to call for every found node
*/
forEach: function(type, callback) {
if (!Array.isArray(this.content)) return;
if (typeof type !== 'string') callback = type, type = null;
var i = 0;
var l = this.content.length;
for (; i < l; i++) {
if (!type || this.content[i] && this.content[i].type === type)
callback(this.content[i], i);
}
},
/**
* @param {Number} index
* @return {Node}
*/
get: function(index) {
return Array.isArray(this.content) && this.content[index];
},
/**
* @param {Number} index
* @param {Node} node
*/
insert: function(index, node) {
Array.isArray(this.content) && this.content.splice(index, 0, node);
},
/**
* @param {String} type
* @return {Boolean} Whether the node is of given type
*/
is: function(type) {
return this.type === type;
},
/**
* @param {String} type
* @return {Node} Last child node
*/
last: function(type) {
var i = this.content.length - 1;
if (!type || !Array.isArray(this.content))
return this.content[i];
for (;;i--) {
if (this.content[i].type === type) return this.content[i];
}
},
/**
* @param {Function} callback
*/
map: function(callback) {
callback(this);
if (!Array.isArray(this.content)) return;
this.content.forEach(function(node) {
if (node instanceof Node)
node.map(callback);
});
},
/**
* @param {Number} index
*/
remove: function(index) {
Array.isArray(this.content) && this.content.splice(index, 1);
},
get length() {
return this.content.length;
},
toString: function() {
return JSON.stringify(this, false, 2);
},
//TODO(tonyganch): Save syntax name while creating a node.
toCSS: function(syntax) {
if (!syntax) return console.error('Empty syntax name.');
try {
stringify = require('./' + syntax + '/stringify');
} catch (e) {
var message = 'Syntax "' + syntax + '" is not supported yet, sorry';
return console.error(message);
}
return stringify(this);
}
};
module.exports = Node;
+42
View File
@@ -0,0 +1,42 @@
var fs = require('fs');
var ParsingError = require('./parsing-error');
var Defaults = {
SYNTAX: 'css',
NEED_INFO: false,
CSS_RULE: 'stylesheet',
JS_RULE: 'program'
};
/**
* @param {String} css
* @param {Object} options
* @return {Object} AST
*/
function parse(css, options) {
if (!css || typeof css !== 'string')
throw new Error('Please, pass a string to parse');
var syntax = options && options.syntax || Defaults.SYNTAX;
var needInfo = options && options.needInfo || Defaults.NEED_INFO;
var rule = options && options.rule ||
(syntax === 'js' ? Defaults.JS_RULE : Defaults.CSS_RULE);
if (!fs.existsSync(__dirname + '/' + syntax))
return console.error('Syntax "' + syntax + '" is not supported yet, sorry');
var getTokens = require('./' + syntax + '/tokenizer');
var mark = require('./' + syntax + '/mark');
var parse = require('./' + syntax + '/parse');
var tokens = getTokens(css);
mark(tokens);
try {
return parse(tokens, rule, needInfo);
} catch (e) {
throw new ParsingError(e, css);
}
}
module.exports = parse;
@@ -0,0 +1,79 @@
var parserPackage = require('../package.json');
/**
* @param {Error} e
* @param {String} css
*/
function ParsingError(e, css) {
this.line = e.line;
this.syntax = e.syntax;
this.css_ = css;
}
ParsingError.prototype = {
/**
* @type {Number}
*/
line: null,
/**
* @type {String}
*/
name: 'Parsing error',
/**
* @type {String}
*/
syntax: null,
/**
* @type {String}
*/
version: parserPackage.version,
/**
* @return {String}
*/
toString: function() {
return this.name + ': ' + this.message;
},
/**
* @type {String}
*/
get message() {
return [
'Please check the validity of the block starting from line #' + this.line,
'',
this.codeFragment_,
'',
'Gonzales PE version: ' + this.version,
'Syntax: ' + this.syntax
].join('\n');
},
/**
* @type {String}
*/
get codeFragment_() {
var LINES_AROUND = 2;
var result = [];
var currentLineNumber = this.line;
var start = currentLineNumber - 1 - LINES_AROUND;
var end = currentLineNumber + LINES_AROUND;
var lines = this.css_.split(/\r\n|\r|\n/);
for (var i = start; i < end; i++) {
var line = lines[i];
if (!line) continue;
var ln = i + 1;
var mark = ln === currentLineNumber ? '*' : ' ';
result.push(ln + mark + '| ' + line);
}
return result.join('\n');
}
};
module.exports = ParsingError;
+182
View File
@@ -0,0 +1,182 @@
var TokenType = require('../token-types');
module.exports = (function() {
/**
* Mark whitespaces and comments
*/
function markSC(tokens) {
var tokensLength = tokens.length,
ws = -1, // flag for whitespaces
sc = -1, // flag for whitespaces and comments
t; // current token
// For every token in the token list, mark spaces and line breaks
// as spaces (set both `ws` and `sc` flags). Mark multiline comments
// with `sc` flag.
// If there are several spaces or tabs or line breaks or multiline
// comments in a row, group them: take the last one's index number
// and save it to the first token in the group as a reference
// (e.g., `ws_last = 7` for a group of whitespaces or `sc_last = 9`
// for a group of whitespaces and comments):
for (var i = 0; i < tokensLength; i++) {
t = tokens[i];
switch (t.type) {
case TokenType.Space:
case TokenType.Tab:
t.ws = true;
t.sc = true;
if (ws === -1) ws = i;
if (sc === -1) sc = i;
break;
case TokenType.Newline:
t.ws = true;
t.sc = true;
ws = ws === -1 ? i : ws;
sc = sc === -1 ? i : ws;
tokens[ws].ws_last = i - 1;
tokens[sc].sc_last = i - 1;
tokens[i].ws_last = i;
tokens[i].sc_last = i;
ws = -1;
sc = -1;
break;
case TokenType.CommentML:
case TokenType.CommentSL:
if (ws !== -1) {
tokens[ws].ws_last = i - 1;
ws = -1;
}
t.sc = true;
break;
default:
if (ws !== -1) {
tokens[ws].ws_last = i - 1;
ws = -1;
}
if (sc !== -1) {
tokens[sc].sc_last = i - 1;
sc = -1;
}
}
}
if (ws !== -1) tokens[ws].ws_last = i - 1;
if (sc !== -1) tokens[sc].sc_last = i - 1;
}
/**
* Pair brackets
*/
function markBrackets(tokens) {
var tokensLength = tokens.length;
var ps = [], // parenthesis
sbs = [], // square brackets
cbs = [], // curly brackets
t; // current token
// For every token in the token list, if we meet an opening (left)
// bracket, push its index number to a corresponding array.
// If we then meet a closing (right) bracket, look at the corresponding
// array. If there are any elements (records about previously met
// left brackets), take a token of the last left bracket (take
// the last index number from the array and find a token with
// this index number) and save right bracket's index as a reference:
for (var i = 0; i < tokens.length; i++) {
t = tokens[i];
switch(t.type) {
case TokenType.LeftParenthesis:
ps.push(i);
break;
case TokenType.RightParenthesis:
if (ps.length) {
t.left = ps.pop();
tokens[t.left].right = i;
}
break;
case TokenType.LeftSquareBracket:
sbs.push(i);
break;
case TokenType.RightSquareBracket:
if (sbs.length) {
t.left = sbs.pop();
tokens[t.left].right = i;
}
break;
case TokenType.LeftCurlyBracket:
cbs.push(i);
break;
case TokenType.RightCurlyBracket:
if (cbs.length) {
t.left = cbs.pop();
tokens[t.left].right = i;
}
break;
}
}
}
function markBlocks(tokens) {
var tokensLength = tokens.length;
var blocks = [],
currentLN = 1,
currentIL = 0,
prevIL = 0,
i = 0,
l = tokens.length,
iw;
for (; i != l; i++) {
if (!tokens[i - 1]) continue;
// Skip all tokens on current line:
if (tokens[i].ln == currentLN) continue;
else currentLN = tokens[i].ln;
// Get indent level:
prevIL = currentIL;
if (tokens[i].type === TokenType.Newline) continue;
else if (tokens[i].type === TokenType.Space &&
tokens[i + 1] &&
tokens[i + 1].type === TokenType.Newline) continue;
else if (tokens[i].type !== TokenType.Space) currentIL = 0;
else {
// If we don't know ident width yet, count number of spaces:
if (!iw) iw = tokens[i].value.length;
prevIL = currentIL;
currentIL = tokens[i].value.length / iw;
}
// Decide whether it's block's start or end:
if (prevIL === currentIL) continue;
else if (currentIL > prevIL) {
blocks.push(i);
continue;
} else {
var il = prevIL;
while (blocks.length > 0 && il !== currentIL) {
tokens[blocks.pop()].block_end = i - 1;
il--;
}
}
}
while (blocks.length > 0) {
tokens[blocks.pop()].block_end = i - 1;
}
}
return function(tokens) {
markBrackets(tokens);
markSC(tokens);
markBlocks(tokens);
}
})();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
module.exports = function stringify(tree) {
// TODO: Better error message
if (!tree) throw new Error('We need tree to translate');
function _t(tree) {
var type = tree.type;
if (_unique[type]) return _unique[type](tree);
if (typeof tree.content === 'string') return tree.content;
if (Array.isArray(tree.content)) return _composite(tree.content);
return '';
}
function _composite(t, i) {
if (!t) return '';
var s = '';
i = i || 0;
for (; i < t.length; i++) s += _t(t[i]);
return s;
}
var _unique = {
'arguments': function(t) {
return '(' + _composite(t.content) + ')';
},
'atkeyword': function(t) {
return '@' + _composite(t.content);
},
'atruler': function(t) {
return _t(t.content[0]) + _t(t.content[1]) + '{' + _t(t.content[2]) + '}';
},
'attribute': function(t) {
return '[' + _composite(t.content) + ']';
},
'block': function(t) {
return _composite(t.content);
},
'braces': function(t) {
return t.content[0] + _composite(t.content.slice(2)) + t.content[1];
},
'class': function(t) {
return '.' + _composite(t.content);
},
'color': function(t) {
return '#' + t.content;
},
'default': function(t) {
return '!' + _composite(t.content) + 'default';
},
'filter': function(t) {
return _t(t.content[0]) + ':' + _t(t.content[1]);
},
'functionExpression': function(t) {
return 'expression(' + t.content + ')';
},
'id': function (t) {
return '#' + t.content;
},
'important': function(t) {
return '!' + _composite(t.content) + 'important';
},
'interpolation': function(t) {
return '#{' + _composite(t.content) + '}';
},
'multilineComment': function (t) {
return '/*' + t.content;
},
'nthSelector': function(t) {
return ':' + _t(t.content[0]) + '(' + _composite(t.content.slice(1)) + ')';
},
'percentage': function(t) {
return _composite(t.content) + '%';
},
'placeholder': function(t) {
return '%' + _composite(t.content);
},
'pseudoClass': function(t) {
return ':' + _composite(t.content);
},
'pseudoElement': function(t) {
return '::' + _composite(t.content);
},
'singlelineComment': function (t) {
return '/' + '/' + t.content;
},
'uri': function(t) {
return 'url(' + _composite(t.content) + ')';
},
'variable': function(t) {
return '$' + _composite(t.content);
},
'variablesList': function(t) {
return _composite(t.content) + '...';
}
};
return _t(tree);
}
@@ -0,0 +1,350 @@
module.exports = function(css) {
var TokenType = require('../token-types');
var tokens = [],
urlMode = false,
blockMode = 0,
c, // current character
cn, // next character
pos = 0,
tn = 0,
ln = 1,
col = 1;
var Punctuation = {
' ': TokenType.Space,
'\n': TokenType.Newline,
'\r': TokenType.Newline,
'\t': TokenType.Tab,
'!': TokenType.ExclamationMark,
'"': TokenType.QuotationMark,
'#': TokenType.NumberSign,
'$': TokenType.DollarSign,
'%': TokenType.PercentSign,
'&': TokenType.Ampersand,
'\'': TokenType.Apostrophe,
'(': TokenType.LeftParenthesis,
')': TokenType.RightParenthesis,
'*': TokenType.Asterisk,
'+': TokenType.PlusSign,
',': TokenType.Comma,
'-': TokenType.HyphenMinus,
'.': TokenType.FullStop,
'/': TokenType.Solidus,
':': TokenType.Colon,
';': TokenType.Semicolon,
'<': TokenType.LessThanSign,
'=': TokenType.EqualsSign,
'==': TokenType.EqualitySign,
'!=': TokenType.InequalitySign,
'>': TokenType.GreaterThanSign,
'?': TokenType.QuestionMark,
'@': TokenType.CommercialAt,
'[': TokenType.LeftSquareBracket,
']': TokenType.RightSquareBracket,
'^': TokenType.CircumflexAccent,
'_': TokenType.LowLine,
'{': TokenType.LeftCurlyBracket,
'|': TokenType.VerticalLine,
'}': TokenType.RightCurlyBracket,
'~': TokenType.Tilde
};
/**
* Add a token to the token list
* @param {string} type
* @param {string} value
*/
function pushToken(type, value, column) {
tokens.push({
tn: tn++,
ln: ln,
col: column,
type: type,
value: value
});
}
/**
* Check if a character is a decimal digit
* @param {string} c Character
* @returns {boolean}
*/
function isDecimalDigit(c) {
return '0123456789'.indexOf(c) >= 0;
}
/**
* Parse spaces
* @param {string} css Unparsed part of CSS string
*/
function parseSpaces(css) {
var start = pos;
// Read the string until we meet a non-space character:
for (; pos < css.length; pos++) {
if (css.charAt(pos) !== ' ') break;
}
// Add a substring containing only spaces to tokens:
pushToken(TokenType.Space, css.substring(start, pos--), col);
col += pos - start;
}
/**
* Parse a string within quotes
* @param {string} css Unparsed part of CSS string
* @param {string} q Quote (either `'` or `"`)
*/
function parseString(css, q) {
var start = pos;
// Read the string until we meet a matching quote:
for (pos++; pos < css.length; pos++) {
// Skip escaped quotes:
if (css.charAt(pos) === '\\') pos++;
else if (css.charAt(pos) === q) break;
}
// Add the string (including quotes) to tokens:
pushToken(q === '"' ? TokenType.StringDQ : TokenType.StringSQ, css.substring(start, pos + 1), col);
col += pos - start;
}
/**
* Parse numbers
* @param {string} css Unparsed part of CSS string
*/
function parseDecimalNumber(css) {
var start = pos;
// Read the string until we meet a character that's not a digit:
for (; pos < css.length; pos++) {
if (!isDecimalDigit(css.charAt(pos))) break;
}
// Add the number to tokens:
pushToken(TokenType.DecimalNumber, css.substring(start, pos--), col);
col += pos - start;
}
/**
* Parse identifier
* @param {string} css Unparsed part of CSS string
*/
function parseIdentifier(css) {
var start = pos;
// Skip all opening slashes:
while (css.charAt(pos) === '/') pos++;
// Read the string until we meet a punctuation mark:
for (; pos < css.length; pos++) {
// Skip all '\':
if (css.charAt(pos) === '\\') pos++;
else if (css.charAt(pos) in Punctuation) break;
}
var ident = css.substring(start, pos--);
// Enter url mode if parsed substring is `url`:
urlMode = urlMode || ident === 'url';
// Add identifier to tokens:
pushToken(TokenType.Identifier, ident, col);
col += pos - start;
}
/**
* Parse equality sign
* @param {string} sass Unparsed part of SASS string
*/
function parseEquality(css) {
pushToken(TokenType.EqualitySign, '==', col);
pos++;
col++;
}
/**
* Parse inequality sign
* @param {string} sass Unparsed part of SASS string
*/
function parseInequality(css) {
pushToken(TokenType.InequalitySign, '!=', col);
pos++;
col++;
}
/**
* Parse a multiline comment
* @param {string} css Unparsed part of CSS string
*/
function parseMLComment(css) {
var start = pos;
// Get current indent level:
var il = 0;
for (var _pos = pos - 1; _pos > -1; _pos--) {
// TODO: Can be tabs:
if (css.charAt(_pos) === ' ') il++;
else break;
}
for (pos += 2; pos < css.length; pos++) {
if (css.charAt(pos) === '\n') {
// Get new line's indent level:
var _il = 0;
for (var _pos = pos + 1; _pos < css.length; _pos++) {
if (css.charAt(_pos) === ' ') _il++;
else break;
}
if (_il > il) {
col = 0;
pos += _pos - pos;
} else break;
}
}
// Add full comment (including `/*`) to the list of tokens:
var comment = css.substring(start, pos + 1);
pushToken(TokenType.CommentML, comment, col);
var newlines = comment.split('\n');
if (newlines.length > 1) {
ln += newlines.length - 1;
col = newlines[newlines.length - 1].length;
} else {
col += (pos - start);
}
}
/**
* Parse a single line comment
* @param {string} css Unparsed part of CSS string
*/
function parseSLComment(css) {
var start = pos;
// Check if comment is the only token on the line, and if so,
// get current indent level:
var il = 0;
var onlyToken = false;
for (var _pos = pos - 1; _pos > -1; _pos--) {
// TODO: Can be tabs:
if (css.charAt(_pos) === ' ') il++;
else if (css.charAt(_pos) === '\n') {
onlyToken = true;
break;
} else break;
}
if (_pos === -1) onlyToken = true;
// Read the string until we meet comment end.
// Since we already know first 2 characters (`//`), start reading
// from `pos + 2`:
if (!onlyToken) {
for (pos += 2; pos < css.length; pos++) {
if (css.charAt(pos) === '\n' || css.charAt(pos) === '\r') {
break;
}
}
} else {
for (pos+=2; pos < css.length; pos++) {
if (css.charAt(pos) === '\n') {
// Get new line's indent level:
var _il = 0;
for (var _pos = pos + 1; _pos < css.length; _pos++) {
if (css.charAt(_pos) === ' ') _il++;
else break;
}
if (_il > il) {
col = 0;
pos += _pos - pos;
} else break;
}
}
}
// Add comment (including `//` and line break) to the list of tokens:
pushToken(TokenType.CommentSL, css.substring(start, pos--), col);
col += pos - start;
}
/**
* Convert a CSS string to a list of tokens
* @param {string} css CSS string
* @returns {Array} List of tokens
* @private
*/
function getTokens(css) {
// Parse string, character by character:
for (pos = 0; pos < css.length; col++, pos++) {
c = css.charAt(pos);
cn = css.charAt(pos + 1);
// If we meet `/*`, it's a start of a multiline comment.
// Parse following characters as a multiline comment:
if (c === '/' && cn === '*') {
parseMLComment(css);
}
// If we meet `//` and it is not a part of url:
else if (!urlMode && c === '/' && cn === '/') {
// If we're currently inside a block, treat `//` as a start
// of identifier. Else treat `//` as a start of a single-line
// comment:
parseSLComment(css);
}
// If current character is a double or single quote, it's a start
// of a string:
else if (c === '"' || c === "'") {
parseString(css, c);
}
// If current character is a space:
else if (c === ' ') {
parseSpaces(css);
}
// If current character is `=`, it must be combined with next `=`
else if (c === '=' && cn === '=') {
parseEquality(css);
}
// If we meet `!=`, this must be inequality
else if (c === '!' && cn === '=') {
parseInequality(css);
}
// If current character is a punctuation mark:
else if (c in Punctuation) {
// Add it to the list of tokens:
pushToken(Punctuation[c], c, col);
if (c === '\n' || c === '\r') {
ln++;
col = 0;
} // Go to next line
if (c === ')') urlMode = false; // exit url mode
if (c === '{') blockMode++; // enter a block
if (c === '}') blockMode--; // exit a block
}
// If current character is a decimal digit:
else if (isDecimalDigit(c)) {
parseDecimalNumber(css);
}
// If current character is anything else:
else {
parseIdentifier(css);
}
}
return tokens;
}
return getTokens(css);
};
+116
View File
@@ -0,0 +1,116 @@
var TokenType = require('../token-types');
module.exports = (function() {
/**
* Mark whitespaces and comments
*/
function markSC(tokens) {
var tokensLength = tokens.length,
ws = -1, // flag for whitespaces
sc = -1, // flag for whitespaces and comments
t; // current token
// For every token in the token list, mark spaces and line breaks
// as spaces (set both `ws` and `sc` flags). Mark multiline comments
// with `sc` flag.
// If there are several spaces or tabs or line breaks or multiline
// comments in a row, group them: take the last one's index number
// and save it to the first token in the group as a reference
// (e.g., `ws_last = 7` for a group of whitespaces or `sc_last = 9`
// for a group of whitespaces and comments):
for (var i = 0; i < tokensLength; i++) {
t = tokens[i];
switch (t.type) {
case TokenType.Space:
case TokenType.Tab:
case TokenType.Newline:
t.ws = true;
t.sc = true;
if (ws === -1) ws = i;
if (sc === -1) sc = i;
break;
case TokenType.CommentML:
case TokenType.CommentSL:
if (ws !== -1) {
tokens[ws].ws_last = i - 1;
ws = -1;
}
t.sc = true;
break;
default:
if (ws !== -1) {
tokens[ws].ws_last = i - 1;
ws = -1;
}
if (sc !== -1) {
tokens[sc].sc_last = i - 1;
sc = -1;
}
}
}
if (ws !== -1) tokens[ws].ws_last = i - 1;
if (sc !== -1) tokens[sc].sc_last = i - 1;
}
/**
* Pair brackets
*/
function markBrackets(tokens) {
var tokensLength = tokens.length;
var ps = [], // parenthesis
sbs = [], // square brackets
cbs = [], // curly brackets
t; // current token
// For every token in the token list, if we meet an opening (left)
// bracket, push its index number to a corresponding array.
// If we then meet a closing (right) bracket, look at the corresponding
// array. If there are any elements (records about previously met
// left brackets), take a token of the last left bracket (take
// the last index number from the array and find a token with
// this index number) and save right bracket's index as a reference:
for (var i = 0; i < tokens.length; i++) {
t = tokens[i];
switch(t.type) {
case TokenType.LeftParenthesis:
ps.push(i);
break;
case TokenType.RightParenthesis:
if (ps.length) {
t.left = ps.pop();
tokens[t.left].right = i;
}
break;
case TokenType.LeftSquareBracket:
sbs.push(i);
break;
case TokenType.RightSquareBracket:
if (sbs.length) {
t.left = sbs.pop();
tokens[t.left].right = i;
}
break;
case TokenType.LeftCurlyBracket:
cbs.push(i);
break;
case TokenType.RightCurlyBracket:
if (cbs.length) {
t.left = cbs.pop();
tokens[t.left].right = i;
}
break;
}
}
}
return function(tokens) {
markBrackets(tokens);
markSC(tokens);
}
})();
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,98 @@
module.exports = function stringify(tree) {
// TODO: Better error message
if (!tree) throw new Error('We need tree to translate');
function _t(tree) {
var type = tree.type;
if (_unique[type]) return _unique[type](tree);
if (typeof tree.content === 'string') return tree.content;
if (Array.isArray(tree.content)) return _composite(tree.content);
return '';
}
function _composite(t, i) {
if (!t) return '';
var s = '';
i = i || 0;
for (; i < t.length; i++) s += _t(t[i]);
return s;
}
var _unique = {
'arguments': function(t) {
return '(' + _composite(t.content) + ')';
},
'atkeyword': function(t) {
return '@' + _composite(t.content);
},
'atruler': function(t) {
return _t(t.content[0]) + _t(t.content[1]) + '{' + _t(t.content[2]) + '}';
},
'attribute': function(t) {
return '[' + _composite(t.content) + ']';
},
'block': function(t) {
return '{' + _composite(t.content) + '}';
},
'braces': function(t) {
return t.content[0] + _composite(t.content.slice(2)) + t.content[1];
},
'class': function(t) {
return '.' + _composite(t.content);
},
'color': function(t) {
return '#' + t.content;
},
'default': function(t) {
return '!' + _composite(t.content) + 'default';
},
'filter': function(t) {
return _t(t.content[0]) + ':' + _t(t.content[1]);
},
'functionExpression': function(t) {
return 'expression(' + t.content + ')';
},
'id': function (t) {
return '#' + t.content;
},
'important': function(t) {
return '!' + _composite(t.content) + 'important';
},
'interpolation': function(t) {
return '#{' + _composite(t.content) + '}';
},
'multilineComment': function (t) {
return '/*' + t.content + '*/';
},
'nthSelector': function(t) {
return ':' + _t(t.content[0]) + '(' + _composite(t.content.slice(1)) + ')';
},
'percentage': function(t) {
return _composite(t.content) + '%';
},
'placeholder': function(t) {
return '%' + _composite(t.content);
},
'pseudoClass': function(t) {
return ':' + _composite(t.content);
},
'pseudoElement': function(t) {
return '::' + _composite(t.content);
},
'singlelineComment': function (t) {
return '/' + '/' + t.content;
},
'uri': function(t) {
return 'url(' + _composite(t.content) + ')';
},
'variable': function(t) {
return '$' + _composite(t.content);
},
'variablesList': function(t) {
return _composite(t.content) + '...';
}
};
return _t(tree);
}
@@ -0,0 +1,305 @@
module.exports = function(css) {
var TokenType = require('../token-types');
var tokens = [],
urlMode = false,
blockMode = 0,
c, // current character
cn, // next character
pos = 0,
tn = 0,
ln = 1,
col = 1;
var Punctuation = {
' ': TokenType.Space,
'\n': TokenType.Newline,
'\r': TokenType.Newline,
'\t': TokenType.Tab,
'!': TokenType.ExclamationMark,
'"': TokenType.QuotationMark,
'#': TokenType.NumberSign,
'$': TokenType.DollarSign,
'%': TokenType.PercentSign,
'&': TokenType.Ampersand,
'\'': TokenType.Apostrophe,
'(': TokenType.LeftParenthesis,
')': TokenType.RightParenthesis,
'*': TokenType.Asterisk,
'+': TokenType.PlusSign,
',': TokenType.Comma,
'-': TokenType.HyphenMinus,
'.': TokenType.FullStop,
'/': TokenType.Solidus,
':': TokenType.Colon,
';': TokenType.Semicolon,
'<': TokenType.LessThanSign,
'=': TokenType.EqualsSign,
'==': TokenType.EqualitySign,
'!=': TokenType.InequalitySign,
'>': TokenType.GreaterThanSign,
'?': TokenType.QuestionMark,
'@': TokenType.CommercialAt,
'[': TokenType.LeftSquareBracket,
']': TokenType.RightSquareBracket,
'^': TokenType.CircumflexAccent,
'_': TokenType.LowLine,
'{': TokenType.LeftCurlyBracket,
'|': TokenType.VerticalLine,
'}': TokenType.RightCurlyBracket,
'~': TokenType.Tilde
};
/**
* Add a token to the token list
* @param {string} type
* @param {string} value
*/
function pushToken(type, value, column) {
tokens.push({
tn: tn++,
ln: ln,
col: column,
type: type,
value: value
});
}
/**
* Check if a character is a decimal digit
* @param {string} c Character
* @returns {boolean}
*/
function isDecimalDigit(c) {
return '0123456789'.indexOf(c) >= 0;
}
/**
* Parse spaces
* @param {string} css Unparsed part of CSS string
*/
function parseSpaces(css) {
var start = pos;
// Read the string until we meet a non-space character:
for (; pos < css.length; pos++) {
if (css.charAt(pos) !== ' ') break;
}
// Add a substring containing only spaces to tokens:
pushToken(TokenType.Space, css.substring(start, pos--), col);
col += pos - start;
}
/**
* Parse a string within quotes
* @param {string} css Unparsed part of CSS string
* @param {string} q Quote (either `'` or `"`)
*/
function parseString(css, q) {
var start = pos;
// Read the string until we meet a matching quote:
for (pos++; pos < css.length; pos++) {
// Skip escaped quotes:
if (css.charAt(pos) === '\\') pos++;
else if (css.charAt(pos) === q) break;
}
// Add the string (including quotes) to tokens:
pushToken(q === '"' ? TokenType.StringDQ : TokenType.StringSQ, css.substring(start, pos + 1), col);
col += pos - start;
}
/**
* Parse numbers
* @param {string} css Unparsed part of CSS string
*/
function parseDecimalNumber(css) {
var start = pos;
// Read the string until we meet a character that's not a digit:
for (; pos < css.length; pos++) {
if (!isDecimalDigit(css.charAt(pos))) break;
}
// Add the number to tokens:
pushToken(TokenType.DecimalNumber, css.substring(start, pos--), col);
col += pos - start;
}
/**
* Parse identifier
* @param {string} css Unparsed part of CSS string
*/
function parseIdentifier(css) {
var start = pos;
// Skip all opening slashes:
while (css.charAt(pos) === '/') pos++;
// Read the string until we meet a punctuation mark:
for (; pos < css.length; pos++) {
// Skip all '\':
if (css.charAt(pos) === '\\') pos++;
else if (css.charAt(pos) in Punctuation) break;
}
var ident = css.substring(start, pos--);
// Enter url mode if parsed substring is `url`:
urlMode = urlMode || ident === 'url';
// Add identifier to tokens:
pushToken(TokenType.Identifier, ident, col);
col += pos - start;
}
/**
* Parse equality sign
* @param {string} sass Unparsed part of SASS string
*/
function parseEquality(css) {
pushToken(TokenType.EqualitySign, '==', col);
pos++;
col++;
}
/**
* Parse inequality sign
* @param {string} sass Unparsed part of SASS string
*/
function parseInequality(css) {
pushToken(TokenType.InequalitySign, '!=', col);
pos++;
col++;
}
/**
* Parse a multiline comment
* @param {string} css Unparsed part of CSS string
*/
function parseMLComment(css) {
var start = pos;
// Read the string until we meet `*/`.
// Since we already know first 2 characters (`/*`), start reading
// from `pos + 2`:
for (pos += 2; pos < css.length; pos++) {
if (css.charAt(pos) === '*' && css.charAt(pos + 1) === '/') {
pos++;
break;
}
}
// Add full comment (including `/*` and `*/`) to the list of tokens:
var comment = css.substring(start, pos + 1);
pushToken(TokenType.CommentML, comment, col);
var newlines = comment.split('\n');
if (newlines.length > 1) {
ln += newlines.length - 1;
col = newlines[newlines.length - 1].length;
} else {
col += (pos - start);
}
}
/**
* Parse a single line comment
* @param {string} css Unparsed part of CSS string
*/
function parseSLComment(css) {
var start = pos;
// Read the string until we meet line break.
// Since we already know first 2 characters (`//`), start reading
// from `pos + 2`:
for (pos += 2; pos < css.length; pos++) {
if (css.charAt(pos) === '\n' || css.charAt(pos) === '\r') {
break;
}
}
// Add comment (including `//` and line break) to the list of tokens:
pushToken(TokenType.CommentSL, css.substring(start, pos--), col);
col += pos - start;
}
/**
* Convert a CSS string to a list of tokens
* @param {string} css CSS string
* @returns {Array} List of tokens
* @private
*/
function getTokens(css) {
// Parse string, character by character:
for (pos = 0; pos < css.length; col++, pos++) {
c = css.charAt(pos);
cn = css.charAt(pos + 1);
// If we meet `/*`, it's a start of a multiline comment.
// Parse following characters as a multiline comment:
if (c === '/' && cn === '*') {
parseMLComment(css);
}
// If we meet `//` and it is not a part of url:
else if (!urlMode && c === '/' && cn === '/') {
// If we're currently inside a block, treat `//` as a start
// of identifier. Else treat `//` as a start of a single-line
// comment:
parseSLComment(css);
}
// If current character is a double or single quote, it's a start
// of a string:
else if (c === '"' || c === "'") {
parseString(css, c);
}
// If current character is a space:
else if (c === ' ') {
parseSpaces(css);
}
// If current character is `=`, it must be combined with next `=`
else if (c === '=' && cn === '=') {
parseEquality(css);
}
// If we meet `!=`, this must be inequality
else if (c === '!' && cn === '=') {
parseInequality(css);
}
// If current character is a punctuation mark:
else if (c in Punctuation) {
// Add it to the list of tokens:
pushToken(Punctuation[c], c, col);
if (c === '\n' || c === '\r') {
ln++;
col = 0;
} // Go to next line
if (c === ')') urlMode = false; // exit url mode
if (c === '{') blockMode++; // enter a block
if (c === '}') blockMode--; // exit a block
}
// If current character is a decimal digit:
else if (isDecimalDigit(c)) {
parseDecimalNumber(css);
}
// If current character is anything else:
else {
parseIdentifier(css);
}
}
return tokens;
}
return getTokens(css);
};
+47
View File
@@ -0,0 +1,47 @@
module.exports = {
StringSQ: 'StringSQ',
StringDQ: 'StringDQ',
CommentML: 'CommentML',
CommentSL: 'CommentSL',
Newline: 'Newline',
Space: 'Space',
Tab: 'Tab',
ExclamationMark: 'ExclamationMark', // !
QuotationMark: 'QuotationMark', // "
NumberSign: 'NumberSign', // #
DollarSign: 'DollarSign', // $
PercentSign: 'PercentSign', // %
Ampersand: 'Ampersand', // &
Apostrophe: 'Apostrophe', // '
LeftParenthesis: 'LeftParenthesis', // (
RightParenthesis: 'RightParenthesis', // )
Asterisk: 'Asterisk', // *
PlusSign: 'PlusSign', // +
Comma: 'Comma', // ,
HyphenMinus: 'HyphenMinus', // -
FullStop: 'FullStop', // .
Solidus: 'Solidus', // /
Colon: 'Colon', // :
Semicolon: 'Semicolon', // ;
LessThanSign: 'LessThanSign', // <
EqualsSign: 'EqualsSign', // =
EqualitySign: 'EqualitySign', // ==
InequalitySign: 'InequalitySign', // !=
GreaterThanSign: 'GreaterThanSign', // >
QuestionMark: 'QuestionMark', // ?
CommercialAt: 'CommercialAt', // @
LeftSquareBracket: 'LeftSquareBracket', // [
ReverseSolidus: 'ReverseSolidus', // \
RightSquareBracket: 'RightSquareBracket', // ]
CircumflexAccent: 'CircumflexAccent', // ^
LowLine: 'LowLine', // _
LeftCurlyBracket: 'LeftCurlyBracket', // {
VerticalLine: 'VerticalLine', // |
RightCurlyBracket: 'RightCurlyBracket', // }
Tilde: 'Tilde', // ~
Identifier: 'Identifier',
DecimalNumber: 'DecimalNumber'
};