match-graph.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. var parse = require('../definition-syntax/parse');
  2. var MATCH = { type: 'Match' };
  3. var MISMATCH = { type: 'Mismatch' };
  4. var DISALLOW_EMPTY = { type: 'DisallowEmpty' };
  5. var LEFTPARENTHESIS = 40; // (
  6. var RIGHTPARENTHESIS = 41; // )
  7. function createCondition(match, thenBranch, elseBranch) {
  8. // reduce node count
  9. if (thenBranch === MATCH && elseBranch === MISMATCH) {
  10. return match;
  11. }
  12. if (match === MATCH && thenBranch === MATCH && elseBranch === MATCH) {
  13. return match;
  14. }
  15. if (match.type === 'If' && match.else === MISMATCH && thenBranch === MATCH) {
  16. thenBranch = match.then;
  17. match = match.match;
  18. }
  19. return {
  20. type: 'If',
  21. match: match,
  22. then: thenBranch,
  23. else: elseBranch
  24. };
  25. }
  26. function isFunctionType(name) {
  27. return (
  28. name.length > 2 &&
  29. name.charCodeAt(name.length - 2) === LEFTPARENTHESIS &&
  30. name.charCodeAt(name.length - 1) === RIGHTPARENTHESIS
  31. );
  32. }
  33. function isEnumCapatible(term) {
  34. return (
  35. term.type === 'Keyword' ||
  36. term.type === 'AtKeyword' ||
  37. term.type === 'Function' ||
  38. term.type === 'Type' && isFunctionType(term.name)
  39. );
  40. }
  41. function buildGroupMatchGraph(combinator, terms, atLeastOneTermMatched) {
  42. switch (combinator) {
  43. case ' ':
  44. // Juxtaposing components means that all of them must occur, in the given order.
  45. //
  46. // a b c
  47. // =
  48. // match a
  49. // then match b
  50. // then match c
  51. // then MATCH
  52. // else MISMATCH
  53. // else MISMATCH
  54. // else MISMATCH
  55. var result = MATCH;
  56. for (var i = terms.length - 1; i >= 0; i--) {
  57. var term = terms[i];
  58. result = createCondition(
  59. term,
  60. result,
  61. MISMATCH
  62. );
  63. };
  64. return result;
  65. case '|':
  66. // A bar (|) separates two or more alternatives: exactly one of them must occur.
  67. //
  68. // a | b | c
  69. // =
  70. // match a
  71. // then MATCH
  72. // else match b
  73. // then MATCH
  74. // else match c
  75. // then MATCH
  76. // else MISMATCH
  77. var result = MISMATCH;
  78. var map = null;
  79. for (var i = terms.length - 1; i >= 0; i--) {
  80. var term = terms[i];
  81. // reduce sequence of keywords into a Enum
  82. if (isEnumCapatible(term)) {
  83. if (map === null && i > 0 && isEnumCapatible(terms[i - 1])) {
  84. map = Object.create(null);
  85. result = createCondition(
  86. {
  87. type: 'Enum',
  88. map: map
  89. },
  90. MATCH,
  91. result
  92. );
  93. }
  94. if (map !== null) {
  95. var key = (isFunctionType(term.name) ? term.name.slice(0, -1) : term.name).toLowerCase();
  96. if (key in map === false) {
  97. map[key] = term;
  98. continue;
  99. }
  100. }
  101. }
  102. map = null;
  103. // create a new conditonal node
  104. result = createCondition(
  105. term,
  106. MATCH,
  107. result
  108. );
  109. };
  110. return result;
  111. case '&&':
  112. // A double ampersand (&&) separates two or more components,
  113. // all of which must occur, in any order.
  114. // Use MatchOnce for groups with a large number of terms,
  115. // since &&-groups produces at least N!-node trees
  116. if (terms.length > 5) {
  117. return {
  118. type: 'MatchOnce',
  119. terms: terms,
  120. all: true
  121. };
  122. }
  123. // Use a combination tree for groups with small number of terms
  124. //
  125. // a && b && c
  126. // =
  127. // match a
  128. // then [b && c]
  129. // else match b
  130. // then [a && c]
  131. // else match c
  132. // then [a && b]
  133. // else MISMATCH
  134. //
  135. // a && b
  136. // =
  137. // match a
  138. // then match b
  139. // then MATCH
  140. // else MISMATCH
  141. // else match b
  142. // then match a
  143. // then MATCH
  144. // else MISMATCH
  145. // else MISMATCH
  146. var result = MISMATCH;
  147. for (var i = terms.length - 1; i >= 0; i--) {
  148. var term = terms[i];
  149. var thenClause;
  150. if (terms.length > 1) {
  151. thenClause = buildGroupMatchGraph(
  152. combinator,
  153. terms.filter(function(newGroupTerm) {
  154. return newGroupTerm !== term;
  155. }),
  156. false
  157. );
  158. } else {
  159. thenClause = MATCH;
  160. }
  161. result = createCondition(
  162. term,
  163. thenClause,
  164. result
  165. );
  166. };
  167. return result;
  168. case '||':
  169. // A double bar (||) separates two or more options:
  170. // one or more of them must occur, in any order.
  171. // Use MatchOnce for groups with a large number of terms,
  172. // since ||-groups produces at least N!-node trees
  173. if (terms.length > 5) {
  174. return {
  175. type: 'MatchOnce',
  176. terms: terms,
  177. all: false
  178. };
  179. }
  180. // Use a combination tree for groups with small number of terms
  181. //
  182. // a || b || c
  183. // =
  184. // match a
  185. // then [b || c]
  186. // else match b
  187. // then [a || c]
  188. // else match c
  189. // then [a || b]
  190. // else MISMATCH
  191. //
  192. // a || b
  193. // =
  194. // match a
  195. // then match b
  196. // then MATCH
  197. // else MATCH
  198. // else match b
  199. // then match a
  200. // then MATCH
  201. // else MATCH
  202. // else MISMATCH
  203. var result = atLeastOneTermMatched ? MATCH : MISMATCH;
  204. for (var i = terms.length - 1; i >= 0; i--) {
  205. var term = terms[i];
  206. var thenClause;
  207. if (terms.length > 1) {
  208. thenClause = buildGroupMatchGraph(
  209. combinator,
  210. terms.filter(function(newGroupTerm) {
  211. return newGroupTerm !== term;
  212. }),
  213. true
  214. );
  215. } else {
  216. thenClause = MATCH;
  217. }
  218. result = createCondition(
  219. term,
  220. thenClause,
  221. result
  222. );
  223. };
  224. return result;
  225. }
  226. }
  227. function buildMultiplierMatchGraph(node) {
  228. var result = MATCH;
  229. var matchTerm = buildMatchGraph(node.term);
  230. if (node.max === 0) {
  231. // disable repeating of empty match to prevent infinite loop
  232. matchTerm = createCondition(
  233. matchTerm,
  234. DISALLOW_EMPTY,
  235. MISMATCH
  236. );
  237. // an occurrence count is not limited, make a cycle;
  238. // to collect more terms on each following matching mismatch
  239. result = createCondition(
  240. matchTerm,
  241. null, // will be a loop
  242. MISMATCH
  243. );
  244. result.then = createCondition(
  245. MATCH,
  246. MATCH,
  247. result // make a loop
  248. );
  249. if (node.comma) {
  250. result.then.else = createCondition(
  251. { type: 'Comma', syntax: node },
  252. result,
  253. MISMATCH
  254. );
  255. }
  256. } else {
  257. // create a match node chain for [min .. max] interval with optional matches
  258. for (var i = node.min || 1; i <= node.max; i++) {
  259. if (node.comma && result !== MATCH) {
  260. result = createCondition(
  261. { type: 'Comma', syntax: node },
  262. result,
  263. MISMATCH
  264. );
  265. }
  266. result = createCondition(
  267. matchTerm,
  268. createCondition(
  269. MATCH,
  270. MATCH,
  271. result
  272. ),
  273. MISMATCH
  274. );
  275. }
  276. }
  277. if (node.min === 0) {
  278. // allow zero match
  279. result = createCondition(
  280. MATCH,
  281. MATCH,
  282. result
  283. );
  284. } else {
  285. // create a match node chain to collect [0 ... min - 1] required matches
  286. for (var i = 0; i < node.min - 1; i++) {
  287. if (node.comma && result !== MATCH) {
  288. result = createCondition(
  289. { type: 'Comma', syntax: node },
  290. result,
  291. MISMATCH
  292. );
  293. }
  294. result = createCondition(
  295. matchTerm,
  296. result,
  297. MISMATCH
  298. );
  299. }
  300. }
  301. return result;
  302. }
  303. function buildMatchGraph(node) {
  304. if (typeof node === 'function') {
  305. return {
  306. type: 'Generic',
  307. fn: node
  308. };
  309. }
  310. switch (node.type) {
  311. case 'Group':
  312. var result = buildGroupMatchGraph(
  313. node.combinator,
  314. node.terms.map(buildMatchGraph),
  315. false
  316. );
  317. if (node.disallowEmpty) {
  318. result = createCondition(
  319. result,
  320. DISALLOW_EMPTY,
  321. MISMATCH
  322. );
  323. }
  324. return result;
  325. case 'Multiplier':
  326. return buildMultiplierMatchGraph(node);
  327. case 'Type':
  328. case 'Property':
  329. return {
  330. type: node.type,
  331. name: node.name,
  332. syntax: node
  333. };
  334. case 'Keyword':
  335. return {
  336. type: node.type,
  337. name: node.name.toLowerCase(),
  338. syntax: node
  339. };
  340. case 'AtKeyword':
  341. return {
  342. type: node.type,
  343. name: '@' + node.name.toLowerCase(),
  344. syntax: node
  345. };
  346. case 'Function':
  347. return {
  348. type: node.type,
  349. name: node.name.toLowerCase() + '(',
  350. syntax: node
  351. };
  352. case 'String':
  353. // convert a one char length String to a Token
  354. if (node.value.length === 3) {
  355. return {
  356. type: 'Token',
  357. value: node.value.charAt(1),
  358. syntax: node
  359. };
  360. }
  361. // otherwise use it as is
  362. return {
  363. type: node.type,
  364. value: node.value.substr(1, node.value.length - 2).replace(/\\'/g, '\''),
  365. syntax: node
  366. };
  367. case 'Token':
  368. return {
  369. type: node.type,
  370. value: node.value,
  371. syntax: node
  372. };
  373. case 'Comma':
  374. return {
  375. type: node.type,
  376. syntax: node
  377. };
  378. default:
  379. throw new Error('Unknown node type:', node.type);
  380. }
  381. }
  382. module.exports = {
  383. MATCH: MATCH,
  384. MISMATCH: MISMATCH,
  385. DISALLOW_EMPTY: DISALLOW_EMPTY,
  386. buildMatchGraph: function(syntaxTree, ref) {
  387. if (typeof syntaxTree === 'string') {
  388. syntaxTree = parse(syntaxTree);
  389. }
  390. return {
  391. type: 'MatchGraph',
  392. match: buildMatchGraph(syntaxTree),
  393. syntax: ref || null,
  394. source: syntaxTree
  395. };
  396. }
  397. };