You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

329 lines
9.5 KiB

var Class = require('./Class');
var trim = require('./trim');
var repeat = require('./repeat');
var defaults = require('./defaults');
var camelCase = require('./camelCase');
exports = {
parse: function(css) {
return new Parser(css).parse();
},
stringify: function(stylesheet, options) {
return new Compiler(stylesheet, options).compile();
}
};
var regComments = /(\/\*[\s\S]*?\*\/)/gi;
var regOpen = /^{\s*/;
var regClose = /^}/;
var regWhitespace = /^\s*/;
var regProperty = /^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/;
var regValue = /^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/;
var regSelector = /^([^{]+)/;
var regSemicolon = /^[;\s]*/;
var regColon = /^:\s*/;
var regMedia = /^@media *([^{]+)/;
var regKeyframes = /^@([-\w]+)?keyframes\s*/;
var regFontFace = /^@font-face\s*/;
var regSupports = /^@supports *([^{]+)/;
var regIdentifier = /^([-\w]+)\s*/;
var regKeyframeSelector = /^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/;
var regComma = /^,\s*/;
var Parser = Class({
initialize: function Parser(css) {
this.input = stripCmt(css);
this.open = this._createMatcher(regOpen);
this.close = this._createMatcher(regClose);
this.whitespace = this._createMatcher(regWhitespace);
this.atImport = this._createAtRule('import');
this.atCharset = this._createAtRule('charset');
this.atNamespace = this._createAtRule('namespace');
},
parse: function() {
return this.stylesheet();
},
stylesheet: function() {
return {
type: 'stylesheet',
rules: this.rules()
};
},
rules: function() {
var rule;
var rules = [];
this.whitespace();
while (
this.input.length &&
this.input[0] !== '}' &&
(rule = this.atRule() || this.rule())
) {
rules.push(rule);
this.whitespace();
}
return rules;
},
atRule: function() {
if (this.input[0] !== '@') return;
return (
this.atKeyframes() ||
this.atMedia() ||
this.atSupports() ||
this.atImport() ||
this.atCharset() ||
this.atNamespace() ||
this.atFontFace()
);
},
atKeyframes: function() {
var matched = this.match(regKeyframes);
if (!matched) return;
var vendor = matched[1] || '';
matched = this.match(regIdentifier);
if (!matched) throw Error('@keyframes missing name');
var name = matched[1];
if (!this.open()) throw Error("@keyframes missing '{'");
var keyframes = [];
var keyframe;
while ((keyframe = this.keyframe())) {
keyframes.push(keyframe);
}
if (!this.close()) throw Error("@keyframes missing '}'");
return {
type: 'keyframes',
name: name,
vendor: vendor,
keyframes: keyframes
};
},
keyframe: function() {
var selector = [];
var matched;
while ((matched = this.match(regKeyframeSelector))) {
selector.push(matched[1]);
this.match(regComma);
}
if (!selector.length) return;
this.whitespace();
return {
type: 'keyframe',
selector: selector.join(', '),
declarations: this.declarations()
};
},
atSupports: function() {
var matched = this.match(regSupports);
if (!matched) return;
var supports = trim(matched[1]);
if (!this.open()) throw Error("@supports missing '{'");
var rules = this.rules();
if (!this.close()) throw Error("@supports missing '}'");
return {
type: 'supports',
supports: supports,
rules: rules
};
},
atFontFace: function() {
var matched = this.match(regFontFace);
if (!matched) return;
if (!this.open()) throw Error("@font-face missing '{'");
var declaration;
var declarations = [];
while ((declaration = this.declaration())) {
declarations.push(declaration);
}
if (!this.close()) throw Error("@font-face missing '}'");
return {
type: 'font-face',
declarations: declarations
};
},
atMedia: function() {
var matched = this.match(regMedia);
if (!matched) return;
var media = trim(matched[1]);
if (!this.open()) throw Error("@media missing '{'");
this.whitespace();
var rules = this.rules();
if (!this.close()) throw Error("@media missing '}'");
return {
type: 'media',
media: media,
rules: rules
};
},
rule: function() {
var selector = this.selector();
if (!selector) throw Error('missing selector');
return {
type: 'rule',
selector: selector,
declarations: this.declarations()
};
},
declarations: function() {
var declarations = [];
if (!this.open()) throw Error("missing '{'");
this.whitespace();
var declaration;
while ((declaration = this.declaration())) {
declarations.push(declaration);
}
if (!this.close()) throw Error("missing '}'");
this.whitespace();
return declarations;
},
declaration: function() {
var property = this.match(regProperty);
if (!property) return;
property = trim(property[0]);
if (!this.match(regColon)) throw Error("property missing ':'");
var value = this.match(regValue);
this.match(regSemicolon);
this.whitespace();
return {
type: 'declaration',
property: property,
value: value ? trim(value[0]) : ''
};
},
selector: function() {
var matched = this.match(regSelector);
if (!matched) return;
return trim(matched[0]);
},
match: function(reg) {
var matched = reg.exec(this.input);
if (!matched) return;
this.input = this.input.slice(matched[0].length);
return matched;
},
_createMatcher: function(reg) {
var _this = this;
return function() {
return _this.match(reg);
};
},
_createAtRule: function(name) {
var reg = new RegExp('^@' + name + '\\s*([^;]+);');
return function() {
var matched = this.match(reg);
if (!matched) return;
var ret = {
type: name
};
ret[name] = trim(matched[1]);
return ret;
};
}
});
var Compiler = Class({
initialize: function Compiler(input) {
var options =
arguments.length > 1 && arguments[1] !== undefined
? arguments[1]
: {};
defaults(options, {
indent: ' '
});
this.input = input;
this.indentLevel = 0;
this.indentation = options.indent;
},
compile: function() {
return this.stylesheet(this.input);
},
stylesheet: function(node) {
return this.mapVisit(node.rules, '\n\n');
},
media: function(node) {
return (
'@media ' +
node.media +
' {\n' +
this.indent(1) +
this.mapVisit(node.rules, '\n\n') +
this.indent(-1) +
'\n}'
);
},
keyframes: function(node) {
return (
'@'.concat(node.vendor, 'keyframes ') +
node.name +
' {\n' +
this.indent(1) +
this.mapVisit(node.keyframes, '\n') +
this.indent(-1) +
'\n}'
);
},
supports: function(node) {
return (
'@supports ' +
node.supports +
' {\n' +
this.indent(1) +
this.mapVisit(node.rules, '\n\n') +
this.indent(-1) +
'\n}'
);
},
keyframe: function(node) {
return this.rule(node);
},
mapVisit: function(nodes, delimiter) {
var str = '';
for (var i = 0, len = nodes.length; i < len; i++) {
var node = nodes[i];
str += this[camelCase(node.type)](node);
if (delimiter && i < len - 1) str += delimiter;
}
return str;
},
fontFace: function(node) {
return (
'@font-face {\n' +
this.indent(1) +
this.mapVisit(node.declarations, '\n') +
this.indent(-1) +
'\n}'
);
},
rule: function(node) {
return (
this.indent() +
node.selector +
' {\n' +
this.indent(1) +
this.mapVisit(node.declarations, '\n') +
this.indent(-1) +
'\n' +
this.indent() +
'}'
);
},
declaration: function(node) {
return this.indent() + node.property + ': ' + node.value + ';';
},
import: function(node) {
return '@import '.concat(node.import, ';');
},
charset: function(node) {
return '@charset '.concat(node.charset, ';');
},
namespace: function(node) {
return '@namespace '.concat(node.namespace, ';');
},
indent: function(level) {
if (level) {
this.indentLevel += level;
return '';
}
return repeat(this.indentation, this.indentLevel);
}
});
var stripCmt = function(str) {
return str.replace(regComments, '');
};
module.exports = exports;