330 lines
9.5 KiB
JavaScript
330 lines
9.5 KiB
JavaScript
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;
|