mirror of
https://github.com/hanzoai/postcss.git
synced 2026-09-24 17:01:56 +00:00
558 lines
16 KiB
JavaScript
558 lines
16 KiB
JavaScript
var CssSyntaxError = require('./css-syntax-error');
|
|
var PreviousMap = require('./previous-map');
|
|
var Declaration = require('./declaration');
|
|
var Comment = require('./comment');
|
|
var AtRule = require('./at-rule');
|
|
var Root = require('./root');
|
|
var Rule = require('./rule');
|
|
|
|
var path = require('path');
|
|
|
|
var isSpace = /\s/;
|
|
|
|
// CSS parser
|
|
class Parser {
|
|
constructor(source, opts = { }) {
|
|
this.source = source.toString();
|
|
this.opts = opts;
|
|
|
|
this.root = new Root();
|
|
this.current = this.root;
|
|
this.parents = [this.current];
|
|
this.type = 'rules';
|
|
this.types = [this.type];
|
|
|
|
this.pos = -1;
|
|
this.line = 1;
|
|
this.lines = [];
|
|
this.column = 0;
|
|
this.buffer = '';
|
|
}
|
|
|
|
loop() {
|
|
this.next = this.source[0];
|
|
var length = this.source.length - 1;
|
|
while ( this.pos < length ) {
|
|
this.move();
|
|
this.nextLetter();
|
|
}
|
|
this.endFile();
|
|
}
|
|
|
|
setMap() {
|
|
var map = new PreviousMap(this.root, this.opts);
|
|
if ( map.text ) {
|
|
this.root.prevMap = map;
|
|
this.root.eachInside( i => i.source.map = map );
|
|
}
|
|
}
|
|
|
|
nextLetter() {
|
|
return this.inString() ||
|
|
this.inComment() ||
|
|
this.isComment() ||
|
|
this.isString() ||
|
|
|
|
this.isWrong() ||
|
|
|
|
this.inAtrule() || this.isAtrule() ||
|
|
this.isBlockEnd() ||
|
|
this.inSelector() || this.isSelector() ||
|
|
this.inProperty() || this.isProperty() || this.inValue();
|
|
}
|
|
|
|
// Parsers
|
|
|
|
inString(close) {
|
|
if ( this.quote ) {
|
|
if ( this.escape && !close ) {
|
|
this.escape = false;
|
|
} else if ( this.letter == '\\' ) {
|
|
this.escape = true;
|
|
} else if ( this.letter == this.quote || close ) {
|
|
this.quote = undefined;
|
|
}
|
|
this.trimmed += this.letter;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
isString() {
|
|
if ( this.letter == '"' || this.letter == "'" ) {
|
|
this.quote = this.letter;
|
|
this.quotePos = { line: this.line, column: this.column };
|
|
this.trimmed += this.letter;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
inComment(close) {
|
|
if ( this.inside('comment') ) {
|
|
if ( close || ( this.letter == '*' && this.next == '/' ) ) {
|
|
var text, left, right;
|
|
[text, left] = this.startSpaces(this.prevBuffer());
|
|
[text, right] = this.endSpaces(text);
|
|
this.current.text = text;
|
|
this.current.left = left;
|
|
this.current.right = right;
|
|
this.move();
|
|
this.pop();
|
|
}
|
|
return true;
|
|
|
|
} else if ( this.inside('value-comment') ) {
|
|
if ( close || ( this.letter == '*' && this.next == '/' ) ) {
|
|
this.popType();
|
|
this.move();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
isComment() {
|
|
if ( this.letter == '/' && this.next == '*' ) {
|
|
if ( this.inside('rules') || this.inside('decls') ) {
|
|
this.init( new Comment() );
|
|
this.addType('comment');
|
|
this.move();
|
|
this.buffer = '';
|
|
} else {
|
|
this.commentPos = { line: this.line, column: this.column };
|
|
this.addType('value-comment');
|
|
this.move();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
isWrong() {
|
|
if ( this.letter == '{' ) {
|
|
if ( this.inside('decls') || this.inside('value') ) {
|
|
this.error("Unexpected {");
|
|
}
|
|
}
|
|
|
|
if ( this.inside('prop') ) {
|
|
if ( this.letter == '}' || this.letter == ';') {
|
|
if ( this.opts.safe ) {
|
|
var string = this.current.before + this.buffer;
|
|
this.current.parent.decls.pop();
|
|
this.pop();
|
|
this.buffer = string;
|
|
this.semicolon = this.prevSemicolon;
|
|
} else {
|
|
this.error('Missing property value');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
isAtrule() {
|
|
if ( this.letter == '@' && this.inside('rules') ) {
|
|
this.init( new AtRule() );
|
|
this.current.name = '';
|
|
this.addType('atrule-name');
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
inAtrule(close) {
|
|
if ( this.inside('atrule-name') ) {
|
|
if ( this.space() ) {
|
|
this.checkAtruleName();
|
|
this.buffer = this.buffer.substr(this.current.name.length);
|
|
this.trimmed = '';
|
|
this.setType('atrule-param');
|
|
|
|
} else if ( this.letter == ';' || this.letter == '{' || close ) {
|
|
this.current.between = '';
|
|
this.checkAtruleName();
|
|
this.endAtruleParams();
|
|
|
|
} else {
|
|
this.current.name += this.letter;
|
|
}
|
|
return true;
|
|
|
|
} else if ( this.inside('atrule-param') ) {
|
|
if ( this.letter == ';' || this.letter == '{' || close ) {
|
|
var raw, left, right;
|
|
[raw, left] = this.startSpaces( this.prevBuffer() );
|
|
[raw, right] = this.endSpaces(raw);
|
|
this.raw('params', this.trimmed.trim(), raw);
|
|
if ( this.current.params ) {
|
|
this.current.afterName = left;
|
|
this.current.between = right;
|
|
} else {
|
|
this.current.afterName = '';
|
|
this.current.between = left + right;
|
|
}
|
|
this.endAtruleParams();
|
|
|
|
} else {
|
|
this.trimmed += this.letter;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
inSelector() {
|
|
if ( this.inside('selector') ) {
|
|
if ( this.letter == '{' ) {
|
|
var raw, spaces;
|
|
[raw, spaces] = this.endSpaces( this.prevBuffer() );
|
|
this.raw('selector', this.trimmed.trim(), raw);
|
|
this.current.between = spaces;
|
|
this.semicolon = false;
|
|
this.buffer = '';
|
|
this.setType('decls');
|
|
} else {
|
|
this.trimmed += this.letter;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
isSelector() {
|
|
if ( !this.space() && this.inside('rules') ) {
|
|
this.init( new Rule() );
|
|
|
|
if ( this.letter == '{' ) {
|
|
this.addType('decls');
|
|
this.current.selector = '';
|
|
this.current.between = '';
|
|
this.semicolon = false;
|
|
this.buffer = '';
|
|
} else {
|
|
this.addType('selector');
|
|
this.buffer = this.letter;
|
|
this.trimmed = this.letter;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
isBlockEnd(close) {
|
|
if ( this.letter == '}' || close ) {
|
|
if ( this.parents.length == 1 ) {
|
|
if ( !this.opts.safe ) {
|
|
this.error('Unexpected }');
|
|
}
|
|
} else {
|
|
if ( this.inside('value') ) {
|
|
this.fixEnd( () => this.inValue('close') );
|
|
} else if ( this.inside('prop') && this.opts.safe ) {
|
|
this.inProperty('close');
|
|
} else {
|
|
if ( this.semicolon ) this.current.semicolon = true;
|
|
this.current.after = this.prevBuffer();
|
|
}
|
|
this.pop();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
inProperty(close) {
|
|
if ( this.inside('prop') ) {
|
|
if ( this.letter == ':' || close ) {
|
|
if ( this.buffer[0] == '*' || this.buffer[0] == '_' ) {
|
|
this.current.before += this.buffer[0];
|
|
this.trimmed = this.trimmed.substr(1);
|
|
this.buffer = this.buffer.substr(1);
|
|
}
|
|
|
|
this.current.prop = this.trimmed.trim();
|
|
var length = this.current.prop.length;
|
|
this.current.between = this.prevBuffer().substr(length);
|
|
this.buffer = '';
|
|
|
|
if ( close ) {
|
|
this.current.value = '';
|
|
this.pop();
|
|
} else {
|
|
this.setType('value');
|
|
}
|
|
this.trimmed = '';
|
|
} else if ( this.letter == '{' ) {
|
|
this.error('Unexpected { in decls');
|
|
} else {
|
|
this.trimmed += this.letter;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
isProperty() {
|
|
if ( this.inside('decls') && !this.space() && this.letter != ';' ) {
|
|
this.init( new Declaration() );
|
|
this.addType('prop');
|
|
this.buffer = this.letter;
|
|
this.trimmed = this.letter;
|
|
this.prevSemicolon = this.semicolon;
|
|
this.semicolon = false;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
inValue(close) {
|
|
if ( this.inside('value') ) {
|
|
if ( this.letter == '(' ) {
|
|
this.inBrackets = true;
|
|
} else if ( this.inBrackets && this.letter == ')' ) {
|
|
this.inBrackets = false;
|
|
}
|
|
|
|
if ( (this.letter == ';' && !this.inBrackets) || close ) {
|
|
if ( this.letter == ';' ) this.semicolon = true;
|
|
|
|
var raw, spaces;
|
|
[raw, spaces] = this.startSpaces(this.prevBuffer());
|
|
var trim = this.trimmed.trim();
|
|
|
|
if ( raw.indexOf('!important') != -1 ) {
|
|
var match = raw.match(/\s+!important\s*$/);
|
|
if ( match ) {
|
|
this.current.important = true;
|
|
this.current._important = match[0];
|
|
raw = raw.slice(0, -match[0].length);
|
|
trim = trim.replace(/\s+!important$/, '');
|
|
}
|
|
}
|
|
|
|
this.raw('value', trim, raw);
|
|
this.current.between += ':' + spaces;
|
|
this.pop();
|
|
} else {
|
|
this.trimmed += this.letter;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
endFile() {
|
|
if ( this.inside('atrule-param') || this.inside('atrule-name') ) {
|
|
this.fixEnd( () => this.inAtrule('close') );
|
|
}
|
|
|
|
if ( this.inside('comment') || this.inside('value-comment') ) {
|
|
if ( this.opts.safe ) {
|
|
this.buffer += '/';
|
|
this.inComment('close');
|
|
this.closeBlocks();
|
|
} else {
|
|
if ( this.inside('comment') ) {
|
|
this.error('Unclosed comment', this.current.source.start);
|
|
} else {
|
|
this.error('Unclosed comment', this.commentPos);
|
|
}
|
|
}
|
|
|
|
} else if ( this.parents.length > 1 ) {
|
|
if ( this.opts.safe ) {
|
|
this.closeBlocks();
|
|
} else {
|
|
this.error('Unclosed block', this.current.source.start);
|
|
}
|
|
|
|
} else if ( this.quote ) {
|
|
if ( this.opts.safe ) {
|
|
this.inString('close');
|
|
this.closeBlocks();
|
|
} else {
|
|
this.error('Unclosed quote', this.quotePos);
|
|
}
|
|
|
|
} else {
|
|
this.root.after = this.buffer;
|
|
}
|
|
}
|
|
|
|
// Helpers
|
|
|
|
error(message, pos = { line: this.line, column: this.column }) {
|
|
throw new CssSyntaxError(message, this.source, pos, this.opts.from);
|
|
}
|
|
|
|
move() {
|
|
this.pos += 1;
|
|
this.column += 1;
|
|
this.letter = this.next;
|
|
this.next = this.source[this.pos + 1];
|
|
this.buffer += this.letter;
|
|
|
|
if ( this.letter == "\n" ) {
|
|
this.lines[this.line] = this.column - 1;
|
|
this.line += 1;
|
|
this.column = 0;
|
|
}
|
|
}
|
|
|
|
prevBuffer() {
|
|
return this.buffer.slice(0, -1);
|
|
}
|
|
|
|
inside(type) {
|
|
return this.type == type;
|
|
}
|
|
|
|
space() {
|
|
return this.letter.trim() === '';
|
|
}
|
|
|
|
init(node) {
|
|
this.current.push(node);
|
|
this.parents.push(node);
|
|
this.current = node;
|
|
|
|
this.current.source = {
|
|
start: {
|
|
line: this.line,
|
|
column: this.column
|
|
},
|
|
content: this.source
|
|
};
|
|
if ( this.opts.from ) {
|
|
this.current.source.file = path.resolve(this.opts.from);
|
|
}
|
|
this.current.before = this.buffer.slice(0, -1);
|
|
this.buffer = '';
|
|
}
|
|
|
|
raw(prop, value, origin) {
|
|
this.current[prop] = value;
|
|
if ( value != origin ) {
|
|
this.current['_' + prop] = { value: value, raw: origin };
|
|
}
|
|
}
|
|
|
|
fixEnd(callback) {
|
|
var start, after;
|
|
if ( this.letter == '}' ) {
|
|
start = this.buffer.search(/\s*\}$/);
|
|
after = this.buffer.slice(start, -1);
|
|
} else {
|
|
start = this.buffer.search(/\s*$/);
|
|
after = this.buffer.substr(start);
|
|
}
|
|
this.buffer = this.buffer.substr(0, start + 1);
|
|
|
|
var el = this.current;
|
|
callback.apply(this);
|
|
|
|
var lines = after.match(/\n/g);
|
|
if ( lines ) {
|
|
el.source.end.line -= lines.length;
|
|
var all = this.lines[el.source.end.line];
|
|
var last = after.indexOf("\n");
|
|
if ( last == -1 ) last = after.length;
|
|
el.source.end.column = all - last;
|
|
} else {
|
|
el.source.end.column -= after.length;
|
|
}
|
|
|
|
this.current.after = after;
|
|
this.buffer = after;
|
|
}
|
|
|
|
pop() {
|
|
this.current.source.end = {
|
|
line: this.line,
|
|
column: this.column
|
|
};
|
|
|
|
this.popType();
|
|
this.parents.pop();
|
|
this.current = this.parents[this.parents.length - 1];
|
|
this.buffer = '';
|
|
}
|
|
|
|
addType(type) {
|
|
this.types.push(type);
|
|
this.type = type;
|
|
}
|
|
|
|
setType(type) {
|
|
this.types[this.types.length - 1] = type;
|
|
this.type = type;
|
|
}
|
|
|
|
popType() {
|
|
this.types.pop();
|
|
this.type = this.types[this.types.length - 1];
|
|
}
|
|
|
|
atruleType() {
|
|
var name = this.current.name.toLowerCase();
|
|
if ( name == 'page' || name == 'font-face' ) {
|
|
return 'decls';
|
|
} else if ( name.slice(-8) == 'viewport' ) {
|
|
return 'decls';
|
|
} else {
|
|
return 'rules';
|
|
}
|
|
}
|
|
|
|
endAtruleParams() {
|
|
if ( this.letter == '{' ) {
|
|
var type = this.atruleType();
|
|
this.current.addMixin(type);
|
|
this.setType(type);
|
|
this.buffer = '';
|
|
} else {
|
|
if ( this.letter == ';' ) this.current.semicolon = true;
|
|
this.pop();
|
|
}
|
|
}
|
|
|
|
checkAtruleName() {
|
|
if ( this.current.name === '' ) this.error('At-rule without name');
|
|
}
|
|
|
|
startSpaces(string) {
|
|
var match = string.match(/^\s+/);
|
|
if ( match ) {
|
|
var pos = match[0].length;
|
|
return [string.substr(pos), match[0]];
|
|
} else {
|
|
return [string, ''];
|
|
}
|
|
}
|
|
|
|
endSpaces(string) {
|
|
var match = string.match(/\s+$/);
|
|
if ( match ) {
|
|
var pos = match[0].length;
|
|
return [string.slice(0, -pos), match[0]];
|
|
} else {
|
|
return [string, ''];
|
|
}
|
|
}
|
|
|
|
closeBlocks() {
|
|
for ( var i = 0; i < this.parents.length; i++ ) {
|
|
this.buffer += '{';
|
|
this.isBlockEnd('close');
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = function (source, opts = { }) {
|
|
if ( opts.map == 'inline' ) opts.map = { inline: true };
|
|
|
|
var parser = new Parser(source, opts);
|
|
parser.loop();
|
|
parser.setMap();
|
|
|
|
return parser.root;
|
|
};
|