blob: add56806cd7335325a25486fef3d8c32208aa99d [file] [log] [blame]
'use strict';
var names = require('../utils/names');
var MULTIPLIER_DEFAULT = {
comma: false,
min: 1,
max: 1
};
function skipSpaces(node) {
while (node !== null && (node.data.type === 'WhiteSpace' || node.data.type === 'Comment')) {
node = node.next;
}
return node;
}
function putResult(buffer, match) {
var type = match.type || match.syntax.type;
// ignore groups
if (type === 'Group') {
buffer.push.apply(buffer, match.match);
} else {
buffer.push(match);
}
}
function matchToJSON() {
return {
type: this.syntax.type,
name: this.syntax.name,
match: this.match,
node: this.node
};
}
function buildMatchNode(badNode, lastNode, next, match) {
if (badNode) {
return {
badNode: badNode,
lastNode: null,
next: null,
match: null
};
}
return {
badNode: null,
lastNode: lastNode,
next: next,
match: match
};
}
function matchGroup(lexer, syntaxNode, node) {
var result = [];
var buffer;
var multiplier = syntaxNode.multiplier || MULTIPLIER_DEFAULT;
var min = multiplier.min;
var max = multiplier.max === 0 ? Infinity : multiplier.max;
var lastCommaTermCount;
var lastComma;
var matchCount = 0;
var lastNode = null;
var badNode = null;
mismatch:
while (matchCount < max) {
node = skipSpaces(node);
buffer = [];
switch (syntaxNode.combinator) {
case '|':
for (var i = 0; i < syntaxNode.terms.length; i++) {
var term = syntaxNode.terms[i];
var res = matchSyntax(lexer, term, node);
if (res.match) {
putResult(buffer, res.match);
node = res.next;
break; // continue matching
} else if (res.badNode) {
badNode = res.badNode;
break mismatch;
} else if (res.lastNode) {
lastNode = res.lastNode;
}
}
if (buffer.length === 0) {
break mismatch; // nothing found -> stop matching
}
break;
case ' ':
var beforeMatchNode = node;
var lastMatchedTerm = null;
var hasTailMatch = false;
var commaMissed = false;
for (var i = 0; i < syntaxNode.terms.length; i++) {
var term = syntaxNode.terms[i];
var res = matchSyntax(lexer, term, node);
if (res.match) {
if (term.type === 'Comma' && i !== 0 && !hasTailMatch) {
// recover cursor to state before last match and stop matching
lastNode = node && node.data;
node = beforeMatchNode;
break mismatch;
}
// non-empty match (res.next will refer to another node)
if (res.next !== node) {
// match should be preceded by a comma
if (commaMissed) {
lastNode = node && node.data;
node = beforeMatchNode;
break mismatch;
}
hasTailMatch = term.type !== 'Comma';
lastMatchedTerm = term;
}
putResult(buffer, res.match);
node = skipSpaces(res.next);
} else if (res.badNode) {
badNode = res.badNode;
break mismatch;
} else {
if (res.lastNode) {
lastNode = res.lastNode;
}
// it's ok when comma doesn't match when no matches yet
// but only if comma is not first or last term
if (term.type === 'Comma' && i !== 0 && i !== syntaxNode.terms.length - 1) {
if (hasTailMatch) {
commaMissed = true;
}
continue;
}
// recover cursor to state before last match and stop matching
lastNode = res.lastNode || (node && node.data);
node = beforeMatchNode;
break mismatch;
}
}
// don't allow empty match when [ ]!
if (!lastMatchedTerm && syntaxNode.disallowEmpty) {
// empty match but shouldn't
// recover cursor to state before last match and stop matching
lastNode = node && node.data;
node = beforeMatchNode;
break mismatch;
}
// don't allow comma at the end but only if last term isn't a comma
if (lastMatchedTerm && lastMatchedTerm.type === 'Comma' && term.type !== 'Comma') {
lastNode = node && node.data;
node = beforeMatchNode;
break mismatch;
}
break;
case '&&':
var beforeMatchNode = node;
var lastMatchedTerm = null;
var terms = syntaxNode.terms.slice();
while (terms.length) {
var wasMatch = false;
var emptyMatched = 0;
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
var res = matchSyntax(lexer, term, node);
if (res.match) {
// non-empty match (res.next will refer to another node)
if (res.next !== node) {
lastMatchedTerm = term;
} else {
emptyMatched++;
continue;
}
wasMatch = true;
terms.splice(i--, 1);
putResult(buffer, res.match);
node = skipSpaces(res.next);
break;
} else if (res.badNode) {
badNode = res.badNode;
break mismatch;
} else if (res.lastNode) {
lastNode = res.lastNode;
}
}
if (!wasMatch) {
// terms left, but they all are optional
if (emptyMatched === terms.length) {
break;
}
// not ok
lastNode = node && node.data;
node = beforeMatchNode;
break mismatch;
}
}
if (!lastMatchedTerm && syntaxNode.disallowEmpty) { // don't allow empty match when [ ]!
// empty match but shouldn't
// recover cursor to state before last match and stop matching
lastNode = node && node.data;
node = beforeMatchNode;
break mismatch;
}
break;
case '||':
var beforeMatchNode = node;
var lastMatchedTerm = null;
var terms = syntaxNode.terms.slice();
while (terms.length) {
var wasMatch = false;
var emptyMatched = 0;
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
var res = matchSyntax(lexer, term, node);
if (res.match) {
// non-empty match (res.next will refer to another node)
if (res.next !== node) {
lastMatchedTerm = term;
} else {
emptyMatched++;
continue;
}
wasMatch = true;
terms.splice(i--, 1);
putResult(buffer, res.match);
node = skipSpaces(res.next);
break;
} else if (res.badNode) {
badNode = res.badNode;
break mismatch;
} else if (res.lastNode) {
lastNode = res.lastNode;
}
}
if (!wasMatch) {
break;
}
}
// don't allow empty match
if (!lastMatchedTerm && (emptyMatched !== terms.length || syntaxNode.disallowEmpty)) {
// empty match but shouldn't
// recover cursor to state before last match and stop matching
lastNode = node && node.data;
node = beforeMatchNode;
break mismatch;
}
break;
}
// flush buffer
result.push.apply(result, buffer);
matchCount++;
if (!node) {
break;
}
if (multiplier.comma) {
if (lastComma && lastCommaTermCount === result.length) {
// nothing match after comma
break mismatch;
}
node = skipSpaces(node);
if (node !== null && node.data.type === 'Operator' && node.data.value === ',') {
result.push({
syntax: syntaxNode,
match: [{
type: 'ASTNode',
node: node.data,
childrenMatch: null
}]
});
lastCommaTermCount = result.length;
lastComma = node;
node = node.next;
} else {
lastNode = node !== null ? node.data : null;
break mismatch;
}
}
}
// console.log(syntaxNode.type, badNode, lastNode);
if (lastComma && lastCommaTermCount === result.length) {
// nothing match after comma
node = lastComma;
result.pop();
}
return buildMatchNode(badNode, lastNode, node, matchCount < min ? null : {
syntax: syntaxNode,
match: result,
toJSON: matchToJSON
});
}
function matchSyntax(lexer, syntaxNode, node) {
var badNode = null;
var lastNode = null;
var match = null;
switch (syntaxNode.type) {
case 'Group':
return matchGroup(lexer, syntaxNode, node);
case 'Function':
// expect a function node
if (!node || node.data.type !== 'Function') {
break;
}
var keyword = names.keyword(node.data.name);
var name = syntaxNode.name.toLowerCase();
// check function name with vendor consideration
if (name !== keyword.name) {
break;
}
var res = matchSyntax(lexer, syntaxNode.children, node.data.children.head);
if (!res.match || res.next) {
badNode = res.badNode || res.lastNode || (res.next ? res.next.data : null) || node.data;
break;
}
match = [{
type: 'ASTNode',
node: node.data,
childrenMatch: res.match.match
}];
// Use node.next instead of res.next here since syntax is matching
// for internal list and it should be completelly matched (res.next is null at this point).
// Therefore function is matched and we are going to next node
node = node.next;
break;
case 'Parentheses':
if (!node || node.data.type !== 'Parentheses') {
break;
}
var res = matchSyntax(lexer, syntaxNode.children, node.data.children.head);
if (!res.match || res.next) {
badNode = res.badNode || res.lastNode || (res.next ? res.next.data : null) || node.data; // TODO: case when res.next === null
break;
}
match = [{
type: 'ASTNode',
node: node.data,
childrenMatch: res.match.match
}];
node = res.next;
break;
case 'Type':
var typeSyntax = lexer.getType(syntaxNode.name);
if (!typeSyntax) {
throw new Error('Unknown syntax type `' + syntaxNode.name + '`');
}
var res = typeSyntax.match(node);
if (!res.match) {
badNode = res && res.badNode; // TODO: case when res.next === null
lastNode = (res && res.lastNode) || (node && node.data);
break;
}
node = res.next;
putResult(match = [], res.match);
if (match.length === 0) {
match = null;
}
break;
case 'Property':
var propertySyntax = lexer.getProperty(syntaxNode.name);
if (!propertySyntax) {
throw new Error('Unknown property `' + syntaxNode.name + '`');
}
var res = propertySyntax.match(node);
if (!res.match) {
badNode = res && res.badNode; // TODO: case when res.next === null
lastNode = (res && res.lastNode) || (node && node.data);
break;
}
node = res.next;
putResult(match = [], res.match);
if (match.length === 0) {
match = null;
}
break;
case 'Keyword':
if (!node) {
break;
}
if (node.data.type === 'Identifier') {
var keyword = names.keyword(node.data.name);
var keywordName = keyword.name;
var name = syntaxNode.name.toLowerCase();
// drop \0 and \9 hack from keyword name
if (keywordName.indexOf('\\') !== -1) {
keywordName = keywordName.replace(/\\[09].*$/, '');
}
if (name !== keywordName) {
break;
}
} else {
// keyword may to be a number (e.g. font-weight: 400 )
if (node.data.type !== 'Number' || node.data.value !== syntaxNode.name) {
break;
}
}
match = [{
type: 'ASTNode',
node: node.data,
childrenMatch: null
}];
node = node.next;
break;
case 'Slash':
case 'Comma':
if (!node || node.data.type !== 'Operator' || node.data.value !== syntaxNode.value) {
break;
}
match = [{
type: 'ASTNode',
node: node.data,
childrenMatch: null
}];
node = node.next;
break;
case 'String':
if (!node || node.data.type !== 'String') {
break;
}
match = [{
type: 'ASTNode',
node: node.data,
childrenMatch: null
}];
node = node.next;
break;
case 'ASTNode':
if (node && syntaxNode.match(node)) {
match = {
type: 'ASTNode',
node: node.data,
childrenMatch: null
};
node = node.next;
}
return buildMatchNode(badNode, lastNode, node, match);
default:
throw new Error('Not implemented yet node type: ' + syntaxNode.type);
}
return buildMatchNode(badNode, lastNode, node, match === null ? null : {
syntax: syntaxNode,
match: match,
toJSON: matchToJSON
});
};
module.exports = matchSyntax;