123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516 |
- '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;
|