mirror of
https://github.com/hanzoai/postcss.git
synced 2026-09-24 17:01:56 +00:00
404 lines
11 KiB
JavaScript
404 lines
11 KiB
JavaScript
var Declaration = require('./declaration');
|
|
var tokenize = require('./tokenize');
|
|
var Comment = require('./comment');
|
|
var AtRule = require('./at-rule');
|
|
var Root = require('./root');
|
|
var Rule = require('./rule');
|
|
|
|
// CSS parser
|
|
class Parser {
|
|
constructor(input) {
|
|
this.input = input;
|
|
|
|
this.root = new Root();
|
|
this.current = this.root;
|
|
this.spaces = '';
|
|
this.semicolon = false;
|
|
|
|
if ( this.input.map ) this.root.prevMap = this.input.map;
|
|
}
|
|
|
|
tokenize() {
|
|
this.tokens = tokenize(this.input);
|
|
}
|
|
|
|
loop() {
|
|
var token;
|
|
while ( (token = this.tokens.shift()) !== undefined ) {
|
|
if ( !this.nextToken(token) ) this.spaces += token[1];
|
|
}
|
|
this.endFile();
|
|
}
|
|
|
|
nextToken(token) {
|
|
return this.word(token) || this.end(token) ||
|
|
this.comment(token) || this.atrule(token) ||
|
|
this.emptyRule(token);
|
|
}
|
|
|
|
comment(token) {
|
|
if ( token[0] != 'comment' ) return false;
|
|
|
|
var node = new Comment();
|
|
this.init(node, token[2]);
|
|
this.setEnd(node, token[3]);
|
|
|
|
var text = token[1].slice(2, -2);
|
|
var match = text.match(/^(\s*)([^]*[^\s])(\s*)$/);
|
|
node.left = match[1];
|
|
node.text = match[2];
|
|
node.right = match[3];
|
|
return true;
|
|
}
|
|
|
|
emptyRule(token) {
|
|
if ( token[0] != '{' ) return;
|
|
|
|
var node = new Rule();
|
|
this.init(node, token[2]);
|
|
node.between = '';
|
|
node.selector = '';
|
|
this.current = node;
|
|
return true;
|
|
}
|
|
|
|
word(start) {
|
|
if ( start[0] != 'word' && start[0] != ':' ) return;
|
|
|
|
var token;
|
|
var colon = false;
|
|
var buffer = [start];
|
|
var end = false;
|
|
while ( true ) {
|
|
token = this.tokens.shift();
|
|
|
|
if ( !token ) {
|
|
end = true;
|
|
break;
|
|
|
|
} else if ( token[0] == ';' ) {
|
|
buffer.push(token);
|
|
if ( colon ) {
|
|
this.decl(buffer);
|
|
return true;
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
} else if ( token[0] == '{' ) {
|
|
this.rule(buffer);
|
|
return true;
|
|
|
|
} else if ( token[0] == '}' ) {
|
|
this.tokens.unshift(token);
|
|
end = true;
|
|
break;
|
|
|
|
} else if ( token[0] == 'at-word' ) {
|
|
this.tokens.unshift(token);
|
|
break;
|
|
|
|
} else {
|
|
if ( token[0] == ':' ) colon = true;
|
|
buffer.push(token);
|
|
}
|
|
}
|
|
|
|
if ( end && colon ) {
|
|
while ( buffer.length ) {
|
|
token = buffer[ buffer.length - 1 ];
|
|
if ( token[0] != 'space' && token[0] != 'comment' ) break;
|
|
this.tokens.unshift( buffer.pop() );
|
|
}
|
|
this.decl(buffer);
|
|
return true;
|
|
}
|
|
|
|
if ( this.input.safe ) {
|
|
this.spaces += this.map(buffer, i => i[1]).join('');
|
|
return true;
|
|
} else {
|
|
this.input.error('Unknown word', start[2]);
|
|
}
|
|
}
|
|
|
|
rule(tokens) {
|
|
var node = new Rule();
|
|
this.init(node, tokens[0][2]);
|
|
|
|
node.between = this.spacesFromEnd(tokens);
|
|
this.raw(node, 'selector', tokens);
|
|
this.current = node;
|
|
}
|
|
|
|
decl(tokens) {
|
|
var node = new Declaration();
|
|
this.init(node);
|
|
|
|
var last = tokens[ tokens.length - 1 ];
|
|
if ( last[0] == ';' ) {
|
|
this.setEnd(node, last[2]);
|
|
this.semicolon = true;
|
|
tokens.pop();
|
|
} else {
|
|
this.setEnd(node, last[3]);
|
|
}
|
|
|
|
while ( tokens[0][0] != 'word' ) {
|
|
node.before += tokens.shift()[1];
|
|
}
|
|
if ( node.source ) node.source.start = tokens[0][2];
|
|
|
|
node.prop = tokens.shift()[1];
|
|
node.between = '';
|
|
|
|
var token;
|
|
while ( tokens.length ) {
|
|
token = tokens.shift();
|
|
node.between += token[1];
|
|
|
|
if ( token[0] == ':' ) {
|
|
break;
|
|
} else if ( token[0] != 'space' && token[0] != 'comment' ) {
|
|
this.unknownWord(node, token);
|
|
}
|
|
}
|
|
|
|
if ( node.prop[0] == '_' || node.prop[0] == '*' ) {
|
|
node.before += node.prop[0];
|
|
node.prop = node.prop.slice(1);
|
|
}
|
|
node.between += this.spacesFromStart(tokens);
|
|
|
|
var i;
|
|
for ( i = 2; i < tokens.length; i++ ) {
|
|
if ( tokens[i][0] == ':' ) this.missedSemicolon(tokens, i);
|
|
}
|
|
|
|
for ( i = tokens.length - 1; i > 0; i-- ) {
|
|
token = tokens[i];
|
|
if ( token[1] == '!important' ) {
|
|
node.important = true;
|
|
var string = this.stringFrom(tokens, i);
|
|
string = this.spacesFromEnd(tokens) + string;
|
|
if ( string != ' !important' ) node._important = string;
|
|
break;
|
|
} else if ( token[0] != 'space' && token[0] != 'comment' ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.raw(node, 'value', tokens);
|
|
}
|
|
|
|
atrule(token) {
|
|
if ( token[0] != 'at-word' ) return;
|
|
|
|
var node = new AtRule();
|
|
node.name = token[1].slice(1);
|
|
if ( node.name === '' ) {
|
|
if ( this.input.safe ) {
|
|
node.name = '';
|
|
} else {
|
|
this.input.error('At-rule without name', token[2]);
|
|
}
|
|
}
|
|
this.init(node, token[2]);
|
|
|
|
var next;
|
|
var last = false;
|
|
var open = false;
|
|
var params = [];
|
|
while ( true ) {
|
|
token = this.tokens.shift();
|
|
if ( !token ) {
|
|
last = true;
|
|
break;
|
|
} else if ( token[0] == ';' ) {
|
|
this.setEnd(node, token[2]);
|
|
this.semicolon = true;
|
|
break;
|
|
} else if ( token[0] == '{' ) {
|
|
open = true;
|
|
break;
|
|
} else {
|
|
params.push(token);
|
|
}
|
|
}
|
|
|
|
node.between = this.spacesFromEnd(params);
|
|
if ( params.length ) {
|
|
node.afterName = this.spacesFromStart(params);
|
|
this.raw(node, 'params', params);
|
|
if ( last ) {
|
|
this.setEnd(node, params[ params.length - 1 ][3]);
|
|
this.spaces = node.between;
|
|
node.between = '';
|
|
}
|
|
} else {
|
|
node.afterName = '';
|
|
node.params = '';
|
|
}
|
|
|
|
if ( open ) {
|
|
node.childs = [];
|
|
this.current = node;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
end(token) {
|
|
if ( token[0] != '}' ) return;
|
|
|
|
if ( this.semicolon ) {
|
|
this.current.semicolon = true;
|
|
this.semicolon = false;
|
|
}
|
|
this.current.after = (this.current.after || '') + this.spaces;
|
|
this.spaces = '';
|
|
|
|
if ( this.current.parent ) {
|
|
this.setEnd(this.current, token[2]);
|
|
this.current = this.current.parent;
|
|
} else if ( !this.input.safe ) {
|
|
this.input.error('Unexpected }', token[2]);
|
|
} else {
|
|
this.current.after += '}';
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
endFile() {
|
|
if ( this.current.parent && !this.input.safe ) {
|
|
var start = this.current.source ? this.current.source.start : null;
|
|
this.input.error('Unclosed block', start);
|
|
}
|
|
|
|
if ( this.semicolon ) this.current.semicolon = true;
|
|
this.current.after = (this.current.after || '') + this.spaces;
|
|
|
|
while ( this.current.parent ) {
|
|
this.current = this.current.parent;
|
|
this.current.after = '';
|
|
}
|
|
}
|
|
|
|
unknownWord(node, token) {
|
|
if ( this.input.safe ) {
|
|
if ( node.source ) node.source.start = token[2];
|
|
node.before += node.prop + node.between;
|
|
node.prop = tokens.shift()[1];
|
|
node.between = '';
|
|
} else {
|
|
this.input.error('Unknown word', token[2]);
|
|
}
|
|
}
|
|
|
|
missedSemicolon(tokens, colon) {
|
|
if ( this.input.safe ) {
|
|
var split;
|
|
for ( split = colon - 1; split >= 0; split-- ) {
|
|
if ( tokens[split][0] == 'word' ) break;
|
|
}
|
|
for ( split -= 1; split >= 0; split-- ) {
|
|
if ( tokens[split][0] != 'space' ) {
|
|
split += 1;
|
|
break;
|
|
}
|
|
}
|
|
var other = tokens.splice(split, tokens.length - split);
|
|
this.decl(other);
|
|
} else {
|
|
var token;
|
|
var founded = 0;
|
|
for ( var j = colon - 1; j >= 0; j-- ) {
|
|
token = tokens[j];
|
|
if ( token[0] != 'space' ) {
|
|
founded += 1;
|
|
if ( founded == 2 ) break;
|
|
}
|
|
}
|
|
this.input.error('Missed semicolon', token[3]);
|
|
}
|
|
}
|
|
|
|
// Helpers
|
|
|
|
init(node, start) {
|
|
this.current.push(node);
|
|
|
|
if ( this.input.withSource ) {
|
|
node.source = { start: start, content: this.input.css };
|
|
if ( this.input.map ) node.source.map = this.input.map;
|
|
if ( this.input.file ) {
|
|
node.source.file = this.input.file;
|
|
} else {
|
|
node.source.id = this.input.id;
|
|
}
|
|
}
|
|
|
|
node.before = this.spaces;
|
|
this.spaces = '';
|
|
this.semicolon = false;
|
|
}
|
|
|
|
setEnd(node, pos) {
|
|
if ( node.source ) node.source.end = pos;
|
|
}
|
|
|
|
raw(node, prop, tokens) {
|
|
var clean = true;
|
|
var value = this.map(tokens, (token) => {
|
|
if ( token[0] == 'comment' ) {
|
|
clean = false;
|
|
return '';
|
|
} else {
|
|
return token[1];
|
|
}
|
|
}).join('');
|
|
if ( !clean ) {
|
|
var origin = tokens.map( i => i[1] ).join('');
|
|
node['_' + prop] = { value: value, raw: origin };
|
|
}
|
|
node[prop] = value;
|
|
}
|
|
|
|
spacesFromEnd(tokens) {
|
|
var next;
|
|
var spaces = '';
|
|
while ( tokens.length ) {
|
|
next = tokens[ tokens.length - 1 ];
|
|
if ( next[0] != 'space' && next[0] != 'comment' ) break;
|
|
spaces += tokens.pop()[1];
|
|
}
|
|
return spaces;
|
|
}
|
|
|
|
spacesFromStart(tokens) {
|
|
var next;
|
|
var spaces = '';
|
|
while ( tokens.length ) {
|
|
next = tokens[0];
|
|
if ( next[0] != 'space' && next[0] != 'comment' ) break;
|
|
spaces += tokens.shift()[1];
|
|
}
|
|
return spaces;
|
|
}
|
|
|
|
stringFrom(tokens, i) {
|
|
var part = tokens.splice(i, tokens.length - i);
|
|
return this.map(part, i => i[1] ).join('');
|
|
}
|
|
|
|
map(array, callback) {
|
|
var mapped = [];
|
|
for ( var i = 0; i < array.length; i++ ) {
|
|
mapped.push(callback(array[i], i));
|
|
}
|
|
return mapped;
|
|
}
|
|
}
|
|
|
|
module.exports = Parser;
|