'use strict'; var hasOwnProperty = Object.prototype.hasOwnProperty; var matchGraph = require('./match-graph'); var MATCH = matchGraph.MATCH; var MISMATCH = matchGraph.MISMATCH; var DISALLOW_EMPTY = matchGraph.DISALLOW_EMPTY; var TOKEN = 1; var OPEN_SYNTAX = 2; var CLOSE_SYNTAX = 3; var EXIT_REASON_MATCH = 'Match'; var EXIT_REASON_MISMATCH = 'Mismatch'; var EXIT_REASON_ITERATION_LIMIT = 'Maximum iteration number exceeded (please fill an issue on https://github.com/csstree/csstree/issues)'; var ITERATION_LIMIT = 10000; var totalIterationCount = 0; function mapList(list, fn) { var result = []; while (list) { result.unshift(fn(list)); list = list.prev; } return result; } function isCommaContextStart(token) { if (token === null) { return true; } token = token.value.charAt(token.value.length - 1); return ( token === ',' || token === '(' || token === '[' || token === '/' ); } function isCommaContextEnd(token) { if (token === null) { return true; } token = token.value.charAt(0); return ( token === ')' || token === ']' || token === '/' ); } function internalMatch(tokens, syntax, syntaxes) { function moveToNextToken() { do { tokenCursor++; token = tokenCursor < tokens.length ? tokens[tokenCursor] : null; } while (token !== null && !/\S/.test(token.value)); } function getNextToken(offset) { var nextIndex = tokenCursor + offset; return nextIndex < tokens.length ? tokens[nextIndex] : null; } function pushThenStack(nextSyntax) { thenStack = { nextSyntax: nextSyntax, matchStack: matchStack, syntaxStack: syntaxStack, prev: thenStack }; } function pushElseStack(nextSyntax) { elseStack = { nextSyntax: nextSyntax, matchStack: matchStack, syntaxStack: syntaxStack, thenStack: thenStack, tokenCursor: tokenCursor, token: token, prev: elseStack }; } function addTokenToMatch() { matchStack = { type: TOKEN, syntax: syntax.syntax, token: token, prev: matchStack }; moveToNextToken(); if (tokenCursor > longestMatch) { longestMatch = tokenCursor; } return matchStack.token; } function openSyntax() { syntaxStack = { syntax: syntax, prev: syntaxStack }; matchStack = { type: OPEN_SYNTAX, syntax: syntax.syntax, token: matchStack.token, prev: matchStack }; } function closeSyntax() { if (matchStack.type === OPEN_SYNTAX) { matchStack = matchStack.prev; } else { matchStack = { type: CLOSE_SYNTAX, syntax: syntaxStack.syntax, token: matchStack.token, prev: matchStack }; } syntaxStack = syntaxStack.prev; } var syntaxStack = null; var thenStack = null; var elseStack = null; var iterationCount = 0; var exitReason = EXIT_REASON_MATCH; var matchStack = { type: 'Stub', syntax: null, token: null, tokenCursor: -1, prev: null }; var longestMatch = 0; var tokenCursor = -1; var token = null; moveToNextToken(); while (true) { // console.log('--\n', // '#' + iterationCount, // require('util').inspect({ // match: mapList(matchStack, x => x.type === TOKEN ? x.token && x.token.value : x.syntax ? x.type + '!' + x.syntax.name : null), // elseStack: mapList(elseStack, x => x.id), // thenStack: mapList(thenStack, x => x.id), // token: token && token.value, // tokenCursor, // syntax // }, { depth: null }) // ); // prevent infinite loop if (++iterationCount === ITERATION_LIMIT) { console.warn('[csstree-match] BREAK after ' + ITERATION_LIMIT + ' iterations'); exitReason = EXIT_REASON_ITERATION_LIMIT; break; } if (syntax === MATCH) { if (thenStack === null) { // turn to MISMATCH when some tokens left unmatched if (token !== null) { // doesn't mismatch if just one token left and it's an IE hack if (tokenCursor !== tokens.length - 1 || (token.value !== '\\0' && token.value !== '\\9')) { syntax = MISMATCH; continue; } } // break the main loop, return a result - MATCH exitReason = EXIT_REASON_MATCH; break; } // go to next syntax (`then` branch) syntax = thenStack.nextSyntax; // check match is not empty if (syntax === DISALLOW_EMPTY) { if (thenStack.matchStack.token === matchStack.token) { syntax = MISMATCH; continue; } else { syntax = MATCH; } } // close syntax if needed while (syntaxStack !== null && thenStack.syntaxStack !== syntaxStack) { closeSyntax(); } // pop stack thenStack = thenStack.prev; continue; } if (syntax === MISMATCH) { if (elseStack === null) { // break the main loop, return a result - MISMATCH exitReason = EXIT_REASON_MISMATCH; break; } // go to next syntax (`else` branch) syntax = elseStack.nextSyntax; // restore all the rest stack states thenStack = elseStack.thenStack; syntaxStack = elseStack.syntaxStack; matchStack = elseStack.matchStack; tokenCursor = elseStack.tokenCursor; token = elseStack.token; // pop stack elseStack = elseStack.prev; continue; } switch (syntax.type) { case 'MatchGraph': syntax = syntax.match; break; case 'If': // IMPORTANT: else stack push must go first, // since it stores the state of thenStack before changes if (syntax.else !== MISMATCH) { pushElseStack(syntax.else); } if (syntax.then !== MATCH) { pushThenStack(syntax.then); } syntax = syntax.match; break; case 'MatchOnce': syntax = { type: 'MatchOnceBuffer', terms: syntax.terms, all: syntax.all, matchStack: matchStack, index: 0, mask: 0 }; break; case 'MatchOnceBuffer': if (syntax.index === syntax.terms.length) { // if no matches during a cycle if (syntax.matchStack === matchStack) { // no matches at all or it's required all terms to be matched if (syntax.mask === 0 || syntax.all) { syntax = MISMATCH; break; } // a partial match is ok syntax = MATCH; break; } else { // start trying to match from the start syntax.index = 0; syntax.matchStack = matchStack; } } for (; syntax.index < syntax.terms.length; syntax.index++) { if ((syntax.mask & (1 << syntax.index)) === 0) { // IMPORTANT: else stack push must go first, // since it stores the state of thenStack before changes pushElseStack(syntax); pushThenStack({ type: 'AddMatchOnce', buffer: syntax }); // match syntax = syntax.terms[syntax.index++]; break; } } break; case 'AddMatchOnce': syntax = syntax.buffer; var newMask = syntax.mask | (1 << (syntax.index - 1)); // all terms are matched if (newMask === (1 << syntax.terms.length) - 1) { syntax = MATCH; continue; } syntax = { type: 'MatchOnceBuffer', terms: syntax.terms, all: syntax.all, matchStack: syntax.matchStack, index: syntax.index, mask: newMask }; break; case 'Enum': var name = token !== null ? token.value.toLowerCase() : ''; // drop \0 and \9 hack from keyword name if (name.indexOf('\\') !== -1) { name = name.replace(/\\[09].*$/, ''); } if (hasOwnProperty.call(syntax.map, name)) { syntax = syntax.map[name]; } else { syntax = MISMATCH; } break; case 'Generic': syntax = syntax.fn(token, addTokenToMatch, getNextToken) ? MATCH : MISMATCH; break; case 'Type': case 'Property': openSyntax(); var syntaxDict = syntax.type === 'Type' ? 'types' : 'properties'; if (hasOwnProperty.call(syntaxes, syntaxDict) && syntaxes[syntaxDict][syntax.name]) { syntax = syntaxes[syntaxDict][syntax.name].match; } else { syntax = undefined; } if (!syntax) { throw new Error( 'Bad syntax reference: ' + (syntaxStack.syntax.type === 'Type' ? '<' + syntaxStack.syntax.name + '>' : '<\'' + syntaxStack.syntax.name + '\'>') ); } break; case 'Keyword': var name = syntax.name; if (token !== null) { var keywordName = token.value; // drop \0 and \9 hack from keyword name if (keywordName.indexOf('\\') !== -1) { keywordName = keywordName.replace(/\\[09].*$/, ''); } if (keywordName.toLowerCase() === name) { addTokenToMatch(); syntax = MATCH; break; } } syntax = MISMATCH; break; case 'AtKeyword': case 'Function': if (token !== null && token.value.toLowerCase() === syntax.name) { addTokenToMatch(); syntax = MATCH; break; } syntax = MISMATCH; break; case 'Token': if (token !== null && token.value === syntax.value) { addTokenToMatch(); syntax = MATCH; break; } syntax = MISMATCH; break; case 'Comma': if (token !== null && token.value === ',') { if (isCommaContextStart(matchStack.token)) { syntax = MISMATCH; } else { addTokenToMatch(); syntax = isCommaContextEnd(token) ? MISMATCH : MATCH; } } else { syntax = isCommaContextStart(matchStack.token) || isCommaContextEnd(token) ? MATCH : MISMATCH; } break; // case 'String': // TODO: strings with length other than 1 char default: throw new Error('Unknown node type: ' + syntax.type); } } totalIterationCount += iterationCount; if (exitReason === EXIT_REASON_MATCH) { while (syntaxStack !== null) { closeSyntax(); } } else { matchStack = null; } return { tokens: tokens, reason: exitReason, iterations: iterationCount, match: matchStack, longestMatch: longestMatch }; } function matchAsList(tokens, matchGraph, syntaxes) { var matchResult = internalMatch(tokens, matchGraph, syntaxes || {}); if (matchResult.match !== null) { matchResult.match = mapList(matchResult.match, function(item) { if (item.type === OPEN_SYNTAX || item.type === CLOSE_SYNTAX) { return { type: item.type, syntax: item.syntax }; } return { syntax: item.syntax, token: item.token && item.token.value, node: item.token && item.token.node }; }).slice(1); } return matchResult; } function matchAsTree(tokens, matchGraph, syntaxes) { var matchResult = internalMatch(tokens, matchGraph, syntaxes || {}); if (matchResult.match === null) { return matchResult; } var cursor = matchResult.match; var host = matchResult.match = { syntax: matchGraph.syntax || null, match: [] }; var stack = [host]; // revert a list var prev = null; var next = null; while (cursor !== null) { next = cursor.prev; cursor.prev = prev; prev = cursor; cursor = next; } // init the cursor to start with 2nd item since 1st is a stub item cursor = prev.prev; // build a tree while (cursor !== null && cursor.syntax !== null) { var entry = cursor; switch (entry.type) { case OPEN_SYNTAX: host.match.push(host = { syntax: entry.syntax, match: [] }); stack.push(host); break; case CLOSE_SYNTAX: stack.pop(); host = stack[stack.length - 1]; break; default: host.match.push({ syntax: entry.syntax || null, token: entry.token.value, node: entry.token.node }); } cursor = cursor.prev; } return matchResult; } module.exports = { matchAsList: matchAsList, matchAsTree: matchAsTree, getTotalIterationCount: function() { return totalIterationCount; } };