match.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. 'use strict';
  2. var names = require('../utils/names');
  3. var MULTIPLIER_DEFAULT = {
  4. comma: false,
  5. min: 1,
  6. max: 1
  7. };
  8. function skipSpaces(node) {
  9. while (node !== null && (node.data.type === 'WhiteSpace' || node.data.type === 'Comment')) {
  10. node = node.next;
  11. }
  12. return node;
  13. }
  14. function putResult(buffer, match) {
  15. var type = match.type || match.syntax.type;
  16. // ignore groups
  17. if (type === 'Group') {
  18. buffer.push.apply(buffer, match.match);
  19. } else {
  20. buffer.push(match);
  21. }
  22. }
  23. function matchToJSON() {
  24. return {
  25. type: this.syntax.type,
  26. name: this.syntax.name,
  27. match: this.match,
  28. node: this.node
  29. };
  30. }
  31. function buildMatchNode(badNode, lastNode, next, match) {
  32. if (badNode) {
  33. return {
  34. badNode: badNode,
  35. lastNode: null,
  36. next: null,
  37. match: null
  38. };
  39. }
  40. return {
  41. badNode: null,
  42. lastNode: lastNode,
  43. next: next,
  44. match: match
  45. };
  46. }
  47. function matchGroup(lexer, syntaxNode, node) {
  48. var result = [];
  49. var buffer;
  50. var multiplier = syntaxNode.multiplier || MULTIPLIER_DEFAULT;
  51. var min = multiplier.min;
  52. var max = multiplier.max === 0 ? Infinity : multiplier.max;
  53. var lastCommaTermCount;
  54. var lastComma;
  55. var matchCount = 0;
  56. var lastNode = null;
  57. var badNode = null;
  58. mismatch:
  59. while (matchCount < max) {
  60. node = skipSpaces(node);
  61. buffer = [];
  62. switch (syntaxNode.combinator) {
  63. case '|':
  64. for (var i = 0; i < syntaxNode.terms.length; i++) {
  65. var term = syntaxNode.terms[i];
  66. var res = matchSyntax(lexer, term, node);
  67. if (res.match) {
  68. putResult(buffer, res.match);
  69. node = res.next;
  70. break; // continue matching
  71. } else if (res.badNode) {
  72. badNode = res.badNode;
  73. break mismatch;
  74. } else if (res.lastNode) {
  75. lastNode = res.lastNode;
  76. }
  77. }
  78. if (buffer.length === 0) {
  79. break mismatch; // nothing found -> stop matching
  80. }
  81. break;
  82. case ' ':
  83. var beforeMatchNode = node;
  84. var lastMatchedTerm = null;
  85. var hasTailMatch = false;
  86. var commaMissed = false;
  87. for (var i = 0; i < syntaxNode.terms.length; i++) {
  88. var term = syntaxNode.terms[i];
  89. var res = matchSyntax(lexer, term, node);
  90. if (res.match) {
  91. if (term.type === 'Comma' && i !== 0 && !hasTailMatch) {
  92. // recover cursor to state before last match and stop matching
  93. lastNode = node && node.data;
  94. node = beforeMatchNode;
  95. break mismatch;
  96. }
  97. // non-empty match (res.next will refer to another node)
  98. if (res.next !== node) {
  99. // match should be preceded by a comma
  100. if (commaMissed) {
  101. lastNode = node && node.data;
  102. node = beforeMatchNode;
  103. break mismatch;
  104. }
  105. hasTailMatch = term.type !== 'Comma';
  106. lastMatchedTerm = term;
  107. }
  108. putResult(buffer, res.match);
  109. node = skipSpaces(res.next);
  110. } else if (res.badNode) {
  111. badNode = res.badNode;
  112. break mismatch;
  113. } else {
  114. if (res.lastNode) {
  115. lastNode = res.lastNode;
  116. }
  117. // it's ok when comma doesn't match when no matches yet
  118. // but only if comma is not first or last term
  119. if (term.type === 'Comma' && i !== 0 && i !== syntaxNode.terms.length - 1) {
  120. if (hasTailMatch) {
  121. commaMissed = true;
  122. }
  123. continue;
  124. }
  125. // recover cursor to state before last match and stop matching
  126. lastNode = res.lastNode || (node && node.data);
  127. node = beforeMatchNode;
  128. break mismatch;
  129. }
  130. }
  131. // don't allow empty match when [ ]!
  132. if (!lastMatchedTerm && syntaxNode.disallowEmpty) {
  133. // empty match but shouldn't
  134. // recover cursor to state before last match and stop matching
  135. lastNode = node && node.data;
  136. node = beforeMatchNode;
  137. break mismatch;
  138. }
  139. // don't allow comma at the end but only if last term isn't a comma
  140. if (lastMatchedTerm && lastMatchedTerm.type === 'Comma' && term.type !== 'Comma') {
  141. lastNode = node && node.data;
  142. node = beforeMatchNode;
  143. break mismatch;
  144. }
  145. break;
  146. case '&&':
  147. var beforeMatchNode = node;
  148. var lastMatchedTerm = null;
  149. var terms = syntaxNode.terms.slice();
  150. while (terms.length) {
  151. var wasMatch = false;
  152. var emptyMatched = 0;
  153. for (var i = 0; i < terms.length; i++) {
  154. var term = terms[i];
  155. var res = matchSyntax(lexer, term, node);
  156. if (res.match) {
  157. // non-empty match (res.next will refer to another node)
  158. if (res.next !== node) {
  159. lastMatchedTerm = term;
  160. } else {
  161. emptyMatched++;
  162. continue;
  163. }
  164. wasMatch = true;
  165. terms.splice(i--, 1);
  166. putResult(buffer, res.match);
  167. node = skipSpaces(res.next);
  168. break;
  169. } else if (res.badNode) {
  170. badNode = res.badNode;
  171. break mismatch;
  172. } else if (res.lastNode) {
  173. lastNode = res.lastNode;
  174. }
  175. }
  176. if (!wasMatch) {
  177. // terms left, but they all are optional
  178. if (emptyMatched === terms.length) {
  179. break;
  180. }
  181. // not ok
  182. lastNode = node && node.data;
  183. node = beforeMatchNode;
  184. break mismatch;
  185. }
  186. }
  187. if (!lastMatchedTerm && syntaxNode.disallowEmpty) { // don't allow empty match when [ ]!
  188. // empty match but shouldn't
  189. // recover cursor to state before last match and stop matching
  190. lastNode = node && node.data;
  191. node = beforeMatchNode;
  192. break mismatch;
  193. }
  194. break;
  195. case '||':
  196. var beforeMatchNode = node;
  197. var lastMatchedTerm = null;
  198. var terms = syntaxNode.terms.slice();
  199. while (terms.length) {
  200. var wasMatch = false;
  201. var emptyMatched = 0;
  202. for (var i = 0; i < terms.length; i++) {
  203. var term = terms[i];
  204. var res = matchSyntax(lexer, term, node);
  205. if (res.match) {
  206. // non-empty match (res.next will refer to another node)
  207. if (res.next !== node) {
  208. lastMatchedTerm = term;
  209. } else {
  210. emptyMatched++;
  211. continue;
  212. }
  213. wasMatch = true;
  214. terms.splice(i--, 1);
  215. putResult(buffer, res.match);
  216. node = skipSpaces(res.next);
  217. break;
  218. } else if (res.badNode) {
  219. badNode = res.badNode;
  220. break mismatch;
  221. } else if (res.lastNode) {
  222. lastNode = res.lastNode;
  223. }
  224. }
  225. if (!wasMatch) {
  226. break;
  227. }
  228. }
  229. // don't allow empty match
  230. if (!lastMatchedTerm && (emptyMatched !== terms.length || syntaxNode.disallowEmpty)) {
  231. // empty match but shouldn't
  232. // recover cursor to state before last match and stop matching
  233. lastNode = node && node.data;
  234. node = beforeMatchNode;
  235. break mismatch;
  236. }
  237. break;
  238. }
  239. // flush buffer
  240. result.push.apply(result, buffer);
  241. matchCount++;
  242. if (!node) {
  243. break;
  244. }
  245. if (multiplier.comma) {
  246. if (lastComma && lastCommaTermCount === result.length) {
  247. // nothing match after comma
  248. break mismatch;
  249. }
  250. node = skipSpaces(node);
  251. if (node !== null && node.data.type === 'Operator' && node.data.value === ',') {
  252. result.push({
  253. syntax: syntaxNode,
  254. match: [{
  255. type: 'ASTNode',
  256. node: node.data,
  257. childrenMatch: null
  258. }]
  259. });
  260. lastCommaTermCount = result.length;
  261. lastComma = node;
  262. node = node.next;
  263. } else {
  264. lastNode = node !== null ? node.data : null;
  265. break mismatch;
  266. }
  267. }
  268. }
  269. // console.log(syntaxNode.type, badNode, lastNode);
  270. if (lastComma && lastCommaTermCount === result.length) {
  271. // nothing match after comma
  272. node = lastComma;
  273. result.pop();
  274. }
  275. return buildMatchNode(badNode, lastNode, node, matchCount < min ? null : {
  276. syntax: syntaxNode,
  277. match: result,
  278. toJSON: matchToJSON
  279. });
  280. }
  281. function matchSyntax(lexer, syntaxNode, node) {
  282. var badNode = null;
  283. var lastNode = null;
  284. var match = null;
  285. switch (syntaxNode.type) {
  286. case 'Group':
  287. return matchGroup(lexer, syntaxNode, node);
  288. case 'Function':
  289. // expect a function node
  290. if (!node || node.data.type !== 'Function') {
  291. break;
  292. }
  293. var keyword = names.keyword(node.data.name);
  294. var name = syntaxNode.name.toLowerCase();
  295. // check function name with vendor consideration
  296. if (name !== keyword.name) {
  297. break;
  298. }
  299. var res = matchSyntax(lexer, syntaxNode.children, node.data.children.head);
  300. if (!res.match || res.next) {
  301. badNode = res.badNode || res.lastNode || (res.next ? res.next.data : null) || node.data;
  302. break;
  303. }
  304. match = [{
  305. type: 'ASTNode',
  306. node: node.data,
  307. childrenMatch: res.match.match
  308. }];
  309. // Use node.next instead of res.next here since syntax is matching
  310. // for internal list and it should be completelly matched (res.next is null at this point).
  311. // Therefore function is matched and we are going to next node
  312. node = node.next;
  313. break;
  314. case 'Parentheses':
  315. if (!node || node.data.type !== 'Parentheses') {
  316. break;
  317. }
  318. var res = matchSyntax(lexer, syntaxNode.children, node.data.children.head);
  319. if (!res.match || res.next) {
  320. badNode = res.badNode || res.lastNode || (res.next ? res.next.data : null) || node.data; // TODO: case when res.next === null
  321. break;
  322. }
  323. match = [{
  324. type: 'ASTNode',
  325. node: node.data,
  326. childrenMatch: res.match.match
  327. }];
  328. node = res.next;
  329. break;
  330. case 'Type':
  331. var typeSyntax = lexer.getType(syntaxNode.name);
  332. if (!typeSyntax) {
  333. throw new Error('Unknown syntax type `' + syntaxNode.name + '`');
  334. }
  335. var res = typeSyntax.match(node);
  336. if (!res.match) {
  337. badNode = res && res.badNode; // TODO: case when res.next === null
  338. lastNode = (res && res.lastNode) || (node && node.data);
  339. break;
  340. }
  341. node = res.next;
  342. putResult(match = [], res.match);
  343. if (match.length === 0) {
  344. match = null;
  345. }
  346. break;
  347. case 'Property':
  348. var propertySyntax = lexer.getProperty(syntaxNode.name);
  349. if (!propertySyntax) {
  350. throw new Error('Unknown property `' + syntaxNode.name + '`');
  351. }
  352. var res = propertySyntax.match(node);
  353. if (!res.match) {
  354. badNode = res && res.badNode; // TODO: case when res.next === null
  355. lastNode = (res && res.lastNode) || (node && node.data);
  356. break;
  357. }
  358. node = res.next;
  359. putResult(match = [], res.match);
  360. if (match.length === 0) {
  361. match = null;
  362. }
  363. break;
  364. case 'Keyword':
  365. if (!node) {
  366. break;
  367. }
  368. if (node.data.type === 'Identifier') {
  369. var keyword = names.keyword(node.data.name);
  370. var keywordName = keyword.name;
  371. var name = syntaxNode.name.toLowerCase();
  372. // drop \0 and \9 hack from keyword name
  373. if (keywordName.indexOf('\\') !== -1) {
  374. keywordName = keywordName.replace(/\\[09].*$/, '');
  375. }
  376. if (name !== keywordName) {
  377. break;
  378. }
  379. } else {
  380. // keyword may to be a number (e.g. font-weight: 400 )
  381. if (node.data.type !== 'Number' || node.data.value !== syntaxNode.name) {
  382. break;
  383. }
  384. }
  385. match = [{
  386. type: 'ASTNode',
  387. node: node.data,
  388. childrenMatch: null
  389. }];
  390. node = node.next;
  391. break;
  392. case 'Slash':
  393. case 'Comma':
  394. if (!node || node.data.type !== 'Operator' || node.data.value !== syntaxNode.value) {
  395. break;
  396. }
  397. match = [{
  398. type: 'ASTNode',
  399. node: node.data,
  400. childrenMatch: null
  401. }];
  402. node = node.next;
  403. break;
  404. case 'String':
  405. if (!node || node.data.type !== 'String') {
  406. break;
  407. }
  408. match = [{
  409. type: 'ASTNode',
  410. node: node.data,
  411. childrenMatch: null
  412. }];
  413. node = node.next;
  414. break;
  415. case 'ASTNode':
  416. if (node && syntaxNode.match(node)) {
  417. match = {
  418. type: 'ASTNode',
  419. node: node.data,
  420. childrenMatch: null
  421. };
  422. node = node.next;
  423. }
  424. return buildMatchNode(badNode, lastNode, node, match);
  425. default:
  426. throw new Error('Not implemented yet node type: ' + syntaxNode.type);
  427. }
  428. return buildMatchNode(badNode, lastNode, node, match === null ? null : {
  429. syntax: syntaxNode,
  430. match: match,
  431. toJSON: matchToJSON
  432. });
  433. };
  434. module.exports = matchSyntax;