|
|
var parse = require('../definition-syntax/parse');
var MATCH = { type: 'Match' }; var MISMATCH = { type: 'Mismatch' }; var DISALLOW_EMPTY = { type: 'DisallowEmpty' }; var LEFTPARENTHESIS = 40; // (
var RIGHTPARENTHESIS = 41; // )
function createCondition(match, thenBranch, elseBranch) { // reduce node count
if (thenBranch === MATCH && elseBranch === MISMATCH) { return match; }
if (match === MATCH && thenBranch === MATCH && elseBranch === MATCH) { return match; }
if (match.type === 'If' && match.else === MISMATCH && thenBranch === MATCH) { thenBranch = match.then; match = match.match; }
return { type: 'If', match: match, then: thenBranch, else: elseBranch }; }
function isFunctionType(name) { return ( name.length > 2 && name.charCodeAt(name.length - 2) === LEFTPARENTHESIS && name.charCodeAt(name.length - 1) === RIGHTPARENTHESIS ); }
function isEnumCapatible(term) { return ( term.type === 'Keyword' || term.type === 'AtKeyword' || term.type === 'Function' || term.type === 'Type' && isFunctionType(term.name) ); }
function buildGroupMatchGraph(combinator, terms, atLeastOneTermMatched) { switch (combinator) { case ' ': // Juxtaposing components means that all of them must occur, in the given order.
//
// a b c
// =
// match a
// then match b
// then match c
// then MATCH
// else MISMATCH
// else MISMATCH
// else MISMATCH
var result = MATCH;
for (var i = terms.length - 1; i >= 0; i--) { var term = terms[i];
result = createCondition( term, result, MISMATCH ); };
return result;
case '|': // A bar (|) separates two or more alternatives: exactly one of them must occur.
//
// a | b | c
// =
// match a
// then MATCH
// else match b
// then MATCH
// else match c
// then MATCH
// else MISMATCH
var result = MISMATCH; var map = null;
for (var i = terms.length - 1; i >= 0; i--) { var term = terms[i];
// reduce sequence of keywords into a Enum
if (isEnumCapatible(term)) { if (map === null && i > 0 && isEnumCapatible(terms[i - 1])) { map = Object.create(null); result = createCondition( { type: 'Enum', map: map }, MATCH, result ); }
if (map !== null) { var key = (isFunctionType(term.name) ? term.name.slice(0, -1) : term.name).toLowerCase(); if (key in map === false) { map[key] = term; continue; } } }
map = null;
// create a new conditonal node
result = createCondition( term, MATCH, result ); };
return result;
case '&&': // A double ampersand (&&) separates two or more components,
// all of which must occur, in any order.
// Use MatchOnce for groups with a large number of terms,
// since &&-groups produces at least N!-node trees
if (terms.length > 5) { return { type: 'MatchOnce', terms: terms, all: true }; }
// Use a combination tree for groups with small number of terms
//
// a && b && c
// =
// match a
// then [b && c]
// else match b
// then [a && c]
// else match c
// then [a && b]
// else MISMATCH
//
// a && b
// =
// match a
// then match b
// then MATCH
// else MISMATCH
// else match b
// then match a
// then MATCH
// else MISMATCH
// else MISMATCH
var result = MISMATCH;
for (var i = terms.length - 1; i >= 0; i--) { var term = terms[i]; var thenClause;
if (terms.length > 1) { thenClause = buildGroupMatchGraph( combinator, terms.filter(function(newGroupTerm) { return newGroupTerm !== term; }), false ); } else { thenClause = MATCH; }
result = createCondition( term, thenClause, result ); };
return result;
case '||': // A double bar (||) separates two or more options:
// one or more of them must occur, in any order.
// Use MatchOnce for groups with a large number of terms,
// since ||-groups produces at least N!-node trees
if (terms.length > 5) { return { type: 'MatchOnce', terms: terms, all: false }; }
// Use a combination tree for groups with small number of terms
//
// a || b || c
// =
// match a
// then [b || c]
// else match b
// then [a || c]
// else match c
// then [a || b]
// else MISMATCH
//
// a || b
// =
// match a
// then match b
// then MATCH
// else MATCH
// else match b
// then match a
// then MATCH
// else MATCH
// else MISMATCH
var result = atLeastOneTermMatched ? MATCH : MISMATCH;
for (var i = terms.length - 1; i >= 0; i--) { var term = terms[i]; var thenClause;
if (terms.length > 1) { thenClause = buildGroupMatchGraph( combinator, terms.filter(function(newGroupTerm) { return newGroupTerm !== term; }), true ); } else { thenClause = MATCH; }
result = createCondition( term, thenClause, result ); };
return result; } }
function buildMultiplierMatchGraph(node) { var result = MATCH; var matchTerm = buildMatchGraph(node.term);
if (node.max === 0) { // disable repeating of empty match to prevent infinite loop
matchTerm = createCondition( matchTerm, DISALLOW_EMPTY, MISMATCH );
// an occurrence count is not limited, make a cycle;
// to collect more terms on each following matching mismatch
result = createCondition( matchTerm, null, // will be a loop
MISMATCH );
result.then = createCondition( MATCH, MATCH, result // make a loop
);
if (node.comma) { result.then.else = createCondition( { type: 'Comma', syntax: node }, result, MISMATCH ); } } else { // create a match node chain for [min .. max] interval with optional matches
for (var i = node.min || 1; i <= node.max; i++) { if (node.comma && result !== MATCH) { result = createCondition( { type: 'Comma', syntax: node }, result, MISMATCH ); }
result = createCondition( matchTerm, createCondition( MATCH, MATCH, result ), MISMATCH ); } }
if (node.min === 0) { // allow zero match
result = createCondition( MATCH, MATCH, result ); } else { // create a match node chain to collect [0 ... min - 1] required matches
for (var i = 0; i < node.min - 1; i++) { if (node.comma && result !== MATCH) { result = createCondition( { type: 'Comma', syntax: node }, result, MISMATCH ); }
result = createCondition( matchTerm, result, MISMATCH ); } }
return result; }
function buildMatchGraph(node) { if (typeof node === 'function') { return { type: 'Generic', fn: node }; }
switch (node.type) { case 'Group': var result = buildGroupMatchGraph( node.combinator, node.terms.map(buildMatchGraph), false );
if (node.disallowEmpty) { result = createCondition( result, DISALLOW_EMPTY, MISMATCH ); }
return result;
case 'Multiplier': return buildMultiplierMatchGraph(node);
case 'Type': case 'Property': return { type: node.type, name: node.name, syntax: node };
case 'Keyword': return { type: node.type, name: node.name.toLowerCase(), syntax: node };
case 'AtKeyword': return { type: node.type, name: '@' + node.name.toLowerCase(), syntax: node };
case 'Function': return { type: node.type, name: node.name.toLowerCase() + '(', syntax: node };
case 'String': // convert a one char length String to a Token
if (node.value.length === 3) { return { type: 'Token', value: node.value.charAt(1), syntax: node }; }
// otherwise use it as is
return { type: node.type, value: node.value.substr(1, node.value.length - 2).replace(/\\'/g, '\''), syntax: node };
case 'Token': return { type: node.type, value: node.value, syntax: node };
case 'Comma': return { type: node.type, syntax: node };
default: throw new Error('Unknown node type:', node.type); } }
module.exports = { MATCH: MATCH, MISMATCH: MISMATCH, DISALLOW_EMPTY: DISALLOW_EMPTY, buildMatchGraph: function(syntaxTree, ref) { if (typeof syntaxTree === 'string') { syntaxTree = parse(syntaxTree); }
return { type: 'MatchGraph', match: buildMatchGraph(syntaxTree), syntax: ref || null, source: syntaxTree }; } };
|