first commit
This commit is contained in:
515
build/node_modules/css-tree/lib/lexer/match.js
generated
vendored
Normal file
515
build/node_modules/css-tree/lib/lexer/match.js
generated
vendored
Normal file
@@ -0,0 +1,515 @@
|
||||
'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.vendor + 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 !== keyword.vendor + 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;
|
||||
Reference in New Issue
Block a user