mirror of
https://github.com/hanzoai/postcss.git
synced 2026-09-24 17:01:56 +00:00
506 lines
14 KiB
JavaScript
506 lines
14 KiB
JavaScript
import Declaration from './declaration';
|
|
import tokenizer from './tokenize';
|
|
import Comment from './comment';
|
|
import AtRule from './at-rule';
|
|
import Root from './root';
|
|
import Rule from './rule';
|
|
|
|
export default class Parser {
|
|
|
|
constructor(input) {
|
|
this.input = input;
|
|
|
|
this.root = new Root();
|
|
this.current = this.root;
|
|
this.spaces = '';
|
|
this.semicolon = false;
|
|
|
|
this.createTokenizer();
|
|
this.root.source = { input, start: { line: 1, column: 1 } };
|
|
}
|
|
|
|
createTokenizer() {
|
|
this.tokenizer = tokenizer(this.input);
|
|
}
|
|
|
|
parse() {
|
|
let token;
|
|
while ( !this.tokenizer.endOfFile() ) {
|
|
token = this.tokenizer.nextToken();
|
|
|
|
switch ( token[0] ) {
|
|
|
|
case 'space':
|
|
this.spaces += token[1];
|
|
break;
|
|
|
|
case ';':
|
|
this.freeSemicolon(token);
|
|
break;
|
|
|
|
case '}':
|
|
this.end(token);
|
|
break;
|
|
|
|
case 'comment':
|
|
this.comment(token);
|
|
break;
|
|
|
|
case 'at-word':
|
|
this.atrule(token);
|
|
break;
|
|
|
|
case '{':
|
|
this.emptyRule(token);
|
|
break;
|
|
|
|
default:
|
|
this.other(token);
|
|
break;
|
|
}
|
|
}
|
|
this.endFile();
|
|
}
|
|
|
|
comment(token) {
|
|
let node = new Comment();
|
|
this.init(node, token[2], token[3]);
|
|
node.source.end = { line: token[4], column: token[5] };
|
|
|
|
let text = token[1].slice(2, -2);
|
|
if ( /^\s*$/.test(text) ) {
|
|
node.text = '';
|
|
node.raws.left = text;
|
|
node.raws.right = '';
|
|
} else {
|
|
let match = text.match(/^(\s*)([^]*[^\s])(\s*)$/);
|
|
node.text = match[2];
|
|
node.raws.left = match[1];
|
|
node.raws.right = match[3];
|
|
}
|
|
}
|
|
|
|
emptyRule(token) {
|
|
let node = new Rule();
|
|
this.init(node, token[2], token[3]);
|
|
node.selector = '';
|
|
node.raws.between = '';
|
|
this.current = node;
|
|
}
|
|
|
|
other(start) {
|
|
let end = false;
|
|
let type = null;
|
|
let colon = false;
|
|
let bracket = null;
|
|
let brackets = [];
|
|
|
|
let tokens = [];
|
|
let token = start;
|
|
while ( token ) {
|
|
type = token[0];
|
|
tokens.push(token);
|
|
|
|
if ( type === '(' || type === '[' ) {
|
|
if ( !bracket ) bracket = token;
|
|
brackets.push(type === '(' ? ')' : ']');
|
|
|
|
} else if ( brackets.length === 0 ) {
|
|
if ( type === ';' ) {
|
|
if ( colon ) {
|
|
this.decl(tokens);
|
|
return;
|
|
} else {
|
|
break;
|
|
}
|
|
|
|
} else if ( type === '{' ) {
|
|
this.rule(tokens);
|
|
return;
|
|
|
|
} else if ( type === '}' ) {
|
|
this.tokenizer.back(tokens.pop());
|
|
end = true;
|
|
break;
|
|
|
|
} else if ( type === ':' ) {
|
|
colon = true;
|
|
}
|
|
|
|
} else if ( type === brackets[brackets.length - 1] ) {
|
|
brackets.pop();
|
|
if ( brackets.length === 0 ) bracket = null;
|
|
}
|
|
|
|
token = this.tokenizer.nextToken();
|
|
}
|
|
|
|
if ( this.tokenizer.endOfFile() ) end = true;
|
|
if ( brackets.length > 0 ) this.unclosedBracket(bracket);
|
|
|
|
if ( end && colon ) {
|
|
while ( tokens.length ) {
|
|
token = tokens[tokens.length - 1][0];
|
|
if ( token !== 'space' && token !== 'comment' ) break;
|
|
this.tokenizer.back(tokens.pop());
|
|
}
|
|
this.decl(tokens);
|
|
return;
|
|
} else {
|
|
this.unknownWord(tokens);
|
|
}
|
|
}
|
|
|
|
rule(tokens) {
|
|
tokens.pop();
|
|
|
|
let node = new Rule();
|
|
this.init(node, tokens[0][2], tokens[0][3]);
|
|
|
|
node.raws.between = this.spacesAndCommentsFromEnd(tokens);
|
|
this.raw(node, 'selector', tokens);
|
|
this.current = node;
|
|
}
|
|
|
|
decl(tokens) {
|
|
let node = new Declaration();
|
|
this.init(node);
|
|
|
|
let last = tokens[tokens.length - 1];
|
|
if ( last[0] === ';' ) {
|
|
this.semicolon = true;
|
|
tokens.pop();
|
|
}
|
|
if ( last[4] ) {
|
|
node.source.end = { line: last[4], column: last[5] };
|
|
} else {
|
|
node.source.end = { line: last[2], column: last[3] };
|
|
}
|
|
|
|
while ( tokens[0][0] !== 'word' ) {
|
|
if ( tokens.length === 1 ) this.unknownWord(tokens);
|
|
node.raws.before += tokens.shift()[1];
|
|
}
|
|
node.source.start = { line: tokens[0][2], column: tokens[0][3] };
|
|
|
|
node.prop = '';
|
|
while ( tokens.length ) {
|
|
let type = tokens[0][0];
|
|
if ( type === ':' || type === 'space' || type === 'comment' ) {
|
|
break;
|
|
}
|
|
node.prop += tokens.shift()[1];
|
|
}
|
|
|
|
node.raws.between = '';
|
|
|
|
let token;
|
|
while ( tokens.length ) {
|
|
token = tokens.shift();
|
|
|
|
if ( token[0] === ':' ) {
|
|
node.raws.between += token[1];
|
|
break;
|
|
} else {
|
|
node.raws.between += token[1];
|
|
}
|
|
}
|
|
|
|
if ( node.prop[0] === '_' || node.prop[0] === '*' ) {
|
|
node.raws.before += node.prop[0];
|
|
node.prop = node.prop.slice(1);
|
|
}
|
|
node.raws.between += this.spacesAndCommentsFromStart(tokens);
|
|
this.precheckMissedSemicolon(tokens);
|
|
|
|
for ( let i = tokens.length - 1; i > 0; i-- ) {
|
|
token = tokens[i];
|
|
if ( token[1].toLowerCase() === '!important' ) {
|
|
node.important = true;
|
|
let string = this.stringFrom(tokens, i);
|
|
string = this.spacesFromEnd(tokens) + string;
|
|
if ( string !== ' !important' ) node.raws.important = string;
|
|
break;
|
|
|
|
} else if (token[1].toLowerCase() === 'important') {
|
|
let cache = tokens.slice(0);
|
|
let str = '';
|
|
for ( let j = i; j > 0; j-- ) {
|
|
let type = cache[j][0];
|
|
if ( str.trim().indexOf('!') === 0 && type !== 'space' ) {
|
|
break;
|
|
}
|
|
str = cache.pop()[1] + str;
|
|
}
|
|
if ( str.trim().indexOf('!') === 0 ) {
|
|
node.important = true;
|
|
node.raws.important = str;
|
|
tokens = cache;
|
|
}
|
|
}
|
|
|
|
if ( token[0] !== 'space' && token[0] !== 'comment' ) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.raw(node, 'value', tokens);
|
|
|
|
if ( node.value.indexOf(':') !== -1 ) this.checkMissedSemicolon(tokens);
|
|
}
|
|
|
|
atrule(token) {
|
|
let node = new AtRule();
|
|
node.name = token[1].slice(1);
|
|
if ( node.name === '' ) {
|
|
this.unnamedAtrule(node, token);
|
|
}
|
|
this.init(node, token[2], token[3]);
|
|
|
|
let prev;
|
|
let shift;
|
|
let last = false;
|
|
let open = false;
|
|
let params = [];
|
|
|
|
while ( !this.tokenizer.endOfFile() ) {
|
|
token = this.tokenizer.nextToken();
|
|
|
|
if ( token[0] === ';' ) {
|
|
node.source.end = { line: token[2], column: token[3] };
|
|
this.semicolon = true;
|
|
break;
|
|
} else if ( token[0] === '{' ) {
|
|
open = true;
|
|
break;
|
|
} else if ( token[0] === '}') {
|
|
if ( params.length > 0 ) {
|
|
shift = params.length - 1;
|
|
prev = params[shift];
|
|
while ( prev && prev[0] === 'space' ) {
|
|
prev = params[--shift];
|
|
}
|
|
if ( prev ) {
|
|
node.source.end = { line: prev[4], column: prev[5] };
|
|
}
|
|
}
|
|
this.end(token);
|
|
break;
|
|
} else {
|
|
params.push(token);
|
|
}
|
|
|
|
if ( this.tokenizer.endOfFile() ) {
|
|
last = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
node.raws.between = this.spacesAndCommentsFromEnd(params);
|
|
if ( params.length ) {
|
|
node.raws.afterName = this.spacesAndCommentsFromStart(params);
|
|
this.raw(node, 'params', params);
|
|
if ( last ) {
|
|
token = params[params.length - 1];
|
|
node.source.end = { line: token[4], column: token[5] };
|
|
this.spaces = node.raws.between;
|
|
node.raws.between = '';
|
|
}
|
|
} else {
|
|
node.raws.afterName = '';
|
|
node.params = '';
|
|
}
|
|
|
|
if ( open ) {
|
|
node.nodes = [];
|
|
this.current = node;
|
|
}
|
|
}
|
|
|
|
end(token) {
|
|
if ( this.current.nodes && this.current.nodes.length ) {
|
|
this.current.raws.semicolon = this.semicolon;
|
|
}
|
|
this.semicolon = false;
|
|
|
|
this.current.raws.after = (this.current.raws.after || '') + this.spaces;
|
|
this.spaces = '';
|
|
|
|
if ( this.current.parent ) {
|
|
this.current.source.end = { line: token[2], column: token[3] };
|
|
this.current = this.current.parent;
|
|
} else {
|
|
this.unexpectedClose(token);
|
|
}
|
|
}
|
|
|
|
endFile() {
|
|
if ( this.current.parent ) this.unclosedBlock();
|
|
if ( this.current.nodes && this.current.nodes.length ) {
|
|
this.current.raws.semicolon = this.semicolon;
|
|
}
|
|
this.current.raws.after = (this.current.raws.after || '') + this.spaces;
|
|
}
|
|
|
|
freeSemicolon(token) {
|
|
this.spaces += token[1];
|
|
if ( this.current.nodes ) {
|
|
let prev = this.current.nodes[this.current.nodes.length - 1];
|
|
if ( prev && prev.type === 'rule' && !prev.raws.ownSemicolon ) {
|
|
prev.raws.ownSemicolon = this.spaces;
|
|
this.spaces = '';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helpers
|
|
|
|
init(node, line, column) {
|
|
this.current.push(node);
|
|
|
|
node.source = { start: { line, column }, input: this.input };
|
|
node.raws.before = this.spaces;
|
|
this.spaces = '';
|
|
if ( node.type !== 'comment' ) this.semicolon = false;
|
|
}
|
|
|
|
raw(node, prop, tokens) {
|
|
let token, type;
|
|
let length = tokens.length;
|
|
let value = '';
|
|
let clean = true;
|
|
for ( let i = 0; i < length; i += 1 ) {
|
|
token = tokens[i];
|
|
type = token[0];
|
|
if ( type === 'comment' || type === 'space' && i === length - 1 ) {
|
|
clean = false;
|
|
} else {
|
|
value += token[1];
|
|
}
|
|
}
|
|
if ( !clean ) {
|
|
let raw = tokens.reduce( (all, i) => all + i[1], '');
|
|
node.raws[prop] = { value, raw };
|
|
}
|
|
node[prop] = value;
|
|
}
|
|
|
|
spacesAndCommentsFromEnd(tokens) {
|
|
let lastTokenType;
|
|
let spaces = '';
|
|
while ( tokens.length ) {
|
|
lastTokenType = tokens[tokens.length - 1][0];
|
|
if ( lastTokenType !== 'space' &&
|
|
lastTokenType !== 'comment' ) break;
|
|
spaces = tokens.pop()[1] + spaces;
|
|
}
|
|
return spaces;
|
|
}
|
|
|
|
spacesAndCommentsFromStart(tokens) {
|
|
let next;
|
|
let spaces = '';
|
|
while ( tokens.length ) {
|
|
next = tokens[0][0];
|
|
if ( next !== 'space' && next !== 'comment' ) break;
|
|
spaces += tokens.shift()[1];
|
|
}
|
|
return spaces;
|
|
}
|
|
|
|
spacesFromEnd(tokens) {
|
|
let lastTokenType;
|
|
let spaces = '';
|
|
while ( tokens.length ) {
|
|
lastTokenType = tokens[tokens.length - 1][0];
|
|
if ( lastTokenType !== 'space' ) break;
|
|
spaces = tokens.pop()[1] + spaces;
|
|
}
|
|
return spaces;
|
|
}
|
|
|
|
stringFrom(tokens, from) {
|
|
let result = '';
|
|
for ( let i = from; i < tokens.length; i++ ) {
|
|
result += tokens[i][1];
|
|
}
|
|
tokens.splice(from, tokens.length - from);
|
|
return result;
|
|
}
|
|
|
|
colon(tokens) {
|
|
let brackets = 0;
|
|
let token, type, prev;
|
|
for ( let i = 0; i < tokens.length; i++ ) {
|
|
token = tokens[i];
|
|
type = token[0];
|
|
|
|
if ( type === '(' ) {
|
|
brackets += 1;
|
|
} else if ( type === ')' ) {
|
|
brackets -= 1;
|
|
} else if ( brackets === 0 && type === ':' ) {
|
|
if ( !prev ) {
|
|
this.doubleColon(token);
|
|
} else if ( prev[0] === 'word' && prev[1] === 'progid' ) {
|
|
continue;
|
|
} else {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
prev = token;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Errors
|
|
|
|
unclosedBracket(bracket) {
|
|
throw this.input.error('Unclosed bracket', bracket[2], bracket[3]);
|
|
}
|
|
|
|
unknownWord(tokens) {
|
|
throw this.input.error('Unknown word', tokens[0][2], tokens[0][3]);
|
|
}
|
|
|
|
unexpectedClose(token) {
|
|
throw this.input.error('Unexpected }', token[2], token[3]);
|
|
}
|
|
|
|
unclosedBlock() {
|
|
let pos = this.current.source.start;
|
|
throw this.input.error('Unclosed block', pos.line, pos.column);
|
|
}
|
|
|
|
doubleColon(token) {
|
|
throw this.input.error('Double colon', token[2], token[3]);
|
|
}
|
|
|
|
unnamedAtrule(node, token) {
|
|
throw this.input.error('At-rule without name', token[2], token[3]);
|
|
}
|
|
|
|
precheckMissedSemicolon(tokens) {
|
|
// Hook for Safe Parser
|
|
tokens;
|
|
}
|
|
|
|
checkMissedSemicolon(tokens) {
|
|
let colon = this.colon(tokens);
|
|
if ( colon === false ) return;
|
|
|
|
let founded = 0;
|
|
let token;
|
|
for ( let j = colon - 1; j >= 0; j-- ) {
|
|
token = tokens[j];
|
|
if ( token[0] !== 'space' ) {
|
|
founded += 1;
|
|
if ( founded === 2 ) break;
|
|
}
|
|
}
|
|
throw this.input.error('Missed semicolon', token[2], token[3]);
|
|
}
|
|
|
|
}
|