| 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 | 
|         }; | 
|     } | 
| }; |