494 lines
8.1 KiB
JavaScript
494 lines
8.1 KiB
JavaScript
|
|
module.exports = function(css, options){
|
|
options = options || {};
|
|
|
|
/**
|
|
* Positional.
|
|
*/
|
|
|
|
var lineno = 1;
|
|
var column = 1;
|
|
|
|
/**
|
|
* Update lineno and column based on `str`.
|
|
*/
|
|
|
|
function updatePosition(str) {
|
|
var lines = str.match(/\n/g);
|
|
if (lines) lineno += lines.length;
|
|
var i = str.lastIndexOf('\n');
|
|
column = ~i ? str.length - i : column + str.length;
|
|
}
|
|
|
|
/**
|
|
* Mark position and patch `node.position`.
|
|
*/
|
|
|
|
function position() {
|
|
var start = { line: lineno, column: column };
|
|
if (!options.position) return positionNoop;
|
|
|
|
return function(node){
|
|
node.position = {
|
|
start: start,
|
|
end: { line: lineno, column: column },
|
|
source: options.source
|
|
};
|
|
|
|
whitespace();
|
|
return node;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return `node`.
|
|
*/
|
|
|
|
function positionNoop(node) {
|
|
whitespace();
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Error `msg`.
|
|
*/
|
|
|
|
function error(msg) {
|
|
var err = new Error(msg + ' near line ' + lineno + ':' + column);
|
|
err.filename = options.source;
|
|
err.line = lineno;
|
|
err.column = column;
|
|
err.source = css;
|
|
throw err;
|
|
}
|
|
|
|
/**
|
|
* Parse stylesheet.
|
|
*/
|
|
|
|
function stylesheet() {
|
|
return {
|
|
type: 'stylesheet',
|
|
stylesheet: {
|
|
rules: rules()
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Opening brace.
|
|
*/
|
|
|
|
function open() {
|
|
return match(/^{\s*/);
|
|
}
|
|
|
|
/**
|
|
* Closing brace.
|
|
*/
|
|
|
|
function close() {
|
|
return match(/^}/);
|
|
}
|
|
|
|
/**
|
|
* Parse ruleset.
|
|
*/
|
|
|
|
function rules() {
|
|
var node;
|
|
var rules = [];
|
|
whitespace();
|
|
comments(rules);
|
|
while (css.charAt(0) != '}' && (node = atrule() || rule())) {
|
|
rules.push(node);
|
|
comments(rules);
|
|
}
|
|
return rules;
|
|
}
|
|
|
|
/**
|
|
* Match `re` and return captures.
|
|
*/
|
|
|
|
function match(re) {
|
|
var m = re.exec(css);
|
|
if (!m) return;
|
|
var str = m[0];
|
|
updatePosition(str);
|
|
css = css.slice(str.length);
|
|
return m;
|
|
}
|
|
|
|
/**
|
|
* Parse whitespace.
|
|
*/
|
|
|
|
function whitespace() {
|
|
match(/^\s*/);
|
|
}
|
|
|
|
/**
|
|
* Parse comments;
|
|
*/
|
|
|
|
function comments(rules) {
|
|
var c;
|
|
rules = rules || [];
|
|
while (c = comment()) rules.push(c);
|
|
return rules;
|
|
}
|
|
|
|
/**
|
|
* Parse comment.
|
|
*/
|
|
|
|
function comment() {
|
|
var pos = position();
|
|
if ('/' != css.charAt(0) || '*' != css.charAt(1)) return;
|
|
|
|
var i = 2;
|
|
while (null != css.charAt(i) && ('*' != css.charAt(i) || '/' != css.charAt(i + 1))) ++i;
|
|
i += 2;
|
|
|
|
var str = css.slice(2, i - 2);
|
|
column += 2;
|
|
updatePosition(str);
|
|
css = css.slice(i);
|
|
column += 2;
|
|
|
|
return pos({
|
|
type: 'comment',
|
|
comment: str
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse selector.
|
|
*/
|
|
|
|
function selector() {
|
|
var m = match(/^([^{]+)/);
|
|
if (!m) return;
|
|
return trim(m[0]).split(/\s*,\s*/);
|
|
}
|
|
|
|
/**
|
|
* Parse declaration.
|
|
*/
|
|
|
|
function declaration() {
|
|
var pos = position();
|
|
|
|
// prop
|
|
var prop = match(/^(\*?[-#\/\*\w]+(\[[0-9a-z_-]+\])?)\s*/);
|
|
if (!prop) return;
|
|
prop = trim(prop[0]);
|
|
|
|
// :
|
|
if (!match(/^:\s*/)) return error("property missing ':'");
|
|
|
|
// val
|
|
var val = match(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)/);
|
|
if (!val) return error('property missing value');
|
|
|
|
var ret = pos({
|
|
type: 'declaration',
|
|
property: prop,
|
|
value: trim(val[0])
|
|
});
|
|
|
|
// ;
|
|
match(/^[;\s]*/);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Parse declarations.
|
|
*/
|
|
|
|
function declarations() {
|
|
var decls = [];
|
|
|
|
if (!open()) return error("missing '{'");
|
|
comments(decls);
|
|
|
|
// declarations
|
|
var decl;
|
|
while (decl = declaration()) {
|
|
decls.push(decl);
|
|
comments(decls);
|
|
}
|
|
|
|
if (!close()) return error("missing '}'");
|
|
return decls;
|
|
}
|
|
|
|
/**
|
|
* Parse keyframe.
|
|
*/
|
|
|
|
function keyframe() {
|
|
var m;
|
|
var vals = [];
|
|
var pos = position();
|
|
|
|
while (m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/)) {
|
|
vals.push(m[1]);
|
|
match(/^,\s*/);
|
|
}
|
|
|
|
if (!vals.length) return;
|
|
|
|
return pos({
|
|
type: 'keyframe',
|
|
values: vals,
|
|
declarations: declarations()
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse keyframes.
|
|
*/
|
|
|
|
function atkeyframes() {
|
|
var pos = position();
|
|
var m = match(/^@([-\w]+)?keyframes */);
|
|
|
|
if (!m) return;
|
|
var vendor = m[1];
|
|
|
|
// identifier
|
|
var m = match(/^([-\w]+)\s*/);
|
|
if (!m) return error("@keyframes missing name");
|
|
var name = m[1];
|
|
|
|
if (!open()) return error("@keyframes missing '{'");
|
|
|
|
var frame;
|
|
var frames = comments();
|
|
while (frame = keyframe()) {
|
|
frames.push(frame);
|
|
frames = frames.concat(comments());
|
|
}
|
|
|
|
if (!close()) return error("@keyframes missing '}'");
|
|
|
|
return pos({
|
|
type: 'keyframes',
|
|
name: name,
|
|
vendor: vendor,
|
|
keyframes: frames
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse supports.
|
|
*/
|
|
|
|
function atsupports() {
|
|
var pos = position();
|
|
var m = match(/^@supports *([^{]+)/);
|
|
|
|
if (!m) return;
|
|
var supports = trim(m[1]);
|
|
|
|
if (!open()) return error("@supports missing '{'");
|
|
|
|
var style = comments().concat(rules());
|
|
|
|
if (!close()) return error("@supports missing '}'");
|
|
|
|
return pos({
|
|
type: 'supports',
|
|
supports: supports,
|
|
rules: style
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse host.
|
|
*/
|
|
|
|
function athost() {
|
|
var pos = position();
|
|
var m = match(/^@host */);
|
|
|
|
if (!m) return;
|
|
|
|
if (!open()) return error("@host missing '{'");
|
|
|
|
var style = comments().concat(rules());
|
|
|
|
if (!close()) return error("@host missing '}'");
|
|
|
|
return pos({
|
|
type: 'host',
|
|
rules: style
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse media.
|
|
*/
|
|
|
|
function atmedia() {
|
|
var pos = position();
|
|
var m = match(/^@media *([^{]+)/);
|
|
|
|
if (!m) return;
|
|
var media = trim(m[1]);
|
|
|
|
if (!open()) return error("@media missing '{'");
|
|
|
|
var style = comments().concat(rules());
|
|
|
|
if (!close()) return error("@media missing '}'");
|
|
|
|
return pos({
|
|
type: 'media',
|
|
media: media,
|
|
rules: style
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse paged media.
|
|
*/
|
|
|
|
function atpage() {
|
|
var pos = position();
|
|
var m = match(/^@page */);
|
|
if (!m) return;
|
|
|
|
var sel = selector() || [];
|
|
|
|
if (!open()) return error("@page missing '{'");
|
|
var decls = comments();
|
|
|
|
// declarations
|
|
var decl;
|
|
while (decl = declaration()) {
|
|
decls.push(decl);
|
|
decls = decls.concat(comments());
|
|
}
|
|
|
|
if (!close()) return error("@page missing '}'");
|
|
|
|
return pos({
|
|
type: 'page',
|
|
selectors: sel,
|
|
declarations: decls
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse document.
|
|
*/
|
|
|
|
function atdocument() {
|
|
var pos = position();
|
|
var m = match(/^@([-\w]+)?document *([^{]+)/);
|
|
if (!m) return;
|
|
|
|
var vendor = trim(m[1]);
|
|
var doc = trim(m[2]);
|
|
|
|
if (!open()) return error("@document missing '{'");
|
|
|
|
var style = comments().concat(rules());
|
|
|
|
if (!close()) return error("@document missing '}'");
|
|
|
|
return pos({
|
|
type: 'document',
|
|
document: doc,
|
|
vendor: vendor,
|
|
rules: style
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parse import
|
|
*/
|
|
|
|
function atimport() {
|
|
return _atrule('import');
|
|
}
|
|
|
|
/**
|
|
* Parse charset
|
|
*/
|
|
|
|
function atcharset() {
|
|
return _atrule('charset');
|
|
}
|
|
|
|
/**
|
|
* Parse namespace
|
|
*/
|
|
|
|
function atnamespace() {
|
|
return _atrule('namespace')
|
|
}
|
|
|
|
/**
|
|
* Parse non-block at-rules
|
|
*/
|
|
|
|
function _atrule(name) {
|
|
var pos = position();
|
|
var m = match(new RegExp('^@' + name + ' *([^;\\n]+);'));
|
|
if (!m) return;
|
|
var ret = { type: name };
|
|
ret[name] = trim(m[1]);
|
|
return pos(ret);
|
|
}
|
|
|
|
/**
|
|
* Parse at rule.
|
|
*/
|
|
|
|
function atrule() {
|
|
if (css[0] != '@') return;
|
|
|
|
return atkeyframes()
|
|
|| atmedia()
|
|
|| atsupports()
|
|
|| atimport()
|
|
|| atcharset()
|
|
|| atnamespace()
|
|
|| atdocument()
|
|
|| atpage()
|
|
|| athost();
|
|
}
|
|
|
|
/**
|
|
* Parse rule.
|
|
*/
|
|
|
|
function rule() {
|
|
var pos = position();
|
|
var sel = selector();
|
|
|
|
if (!sel) return;
|
|
comments();
|
|
|
|
return pos({
|
|
type: 'rule',
|
|
selectors: sel,
|
|
declarations: declarations()
|
|
});
|
|
}
|
|
|
|
return stylesheet();
|
|
};
|
|
|
|
/**
|
|
* Trim `str`.
|
|
*/
|
|
|
|
function trim(str) {
|
|
return str ? str.replace(/^\s+|\s+$/g, '') : '';
|
|
}
|