You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

239 lines
5.3 KiB

4 years ago
  1. "use strict";
  2. var Parser = require("fastparse");
  3. var regexpu = require("regexpu-core");
  4. function unescape(str) {
  5. return str.replace(/\\(.)/g, "$1");
  6. }
  7. function commentMatch(match, content) {
  8. this.selector.nodes.push({
  9. type: "comment",
  10. content: content
  11. });
  12. }
  13. function typeMatch(type) {
  14. return function(match, name) {
  15. this.selector.nodes.push({
  16. type: type,
  17. name: unescape(name)
  18. });
  19. };
  20. }
  21. function pseudoClassStartMatch(match, name) {
  22. var newToken = {
  23. type: "pseudo-class",
  24. name: unescape(name),
  25. content: ""
  26. };
  27. this.selector.nodes.push(newToken);
  28. this.token = newToken;
  29. this.brackets = 1;
  30. return "inBrackets";
  31. }
  32. function nestedPseudoClassStartMatch(match, name, after) {
  33. var newSelector = {
  34. type: "selector",
  35. nodes: []
  36. };
  37. var newToken = {
  38. type: "nested-pseudo-class",
  39. name: unescape(name),
  40. nodes: [newSelector]
  41. };
  42. if(after) {
  43. newSelector.before = after;
  44. }
  45. this.selector.nodes.push(newToken);
  46. this.stack.push(this.root);
  47. this.root = newToken;
  48. this.selector = newSelector;
  49. }
  50. function nestedEnd(match, before) {
  51. if(this.stack.length > 0) {
  52. if(before) {
  53. this.selector.after = before;
  54. }
  55. this.root = this.stack.pop();
  56. this.selector = this.root.nodes[this.root.nodes.length - 1];
  57. } else {
  58. this.selector.nodes.push({
  59. type: "invalid",
  60. value: match
  61. });
  62. }
  63. }
  64. function operatorMatch(match, before, operator, after) {
  65. var token = {
  66. type: "operator",
  67. operator: operator
  68. };
  69. if(before) {
  70. token.before = before;
  71. }
  72. if(after) {
  73. token.after = after;
  74. }
  75. this.selector.nodes.push(token);
  76. }
  77. function spacingMatch(match) {
  78. this.selector.nodes.push({
  79. type: "spacing",
  80. value: match
  81. });
  82. }
  83. function elementMatch(match, namespace, name) {
  84. var newToken = {
  85. type: "element",
  86. name: unescape(name)
  87. };
  88. if(namespace) {
  89. newToken.namespace = unescape(namespace.substr(0, namespace.length - 1));
  90. }
  91. this.selector.nodes.push(newToken);
  92. }
  93. function universalMatch(match, namespace) {
  94. var newToken = {
  95. type: "universal"
  96. };
  97. if(namespace) {
  98. newToken.namespace = unescape(namespace.substr(0, namespace.length - 1));
  99. }
  100. this.selector.nodes.push(newToken);
  101. }
  102. function attributeMatch(match, content) {
  103. this.selector.nodes.push({
  104. type: "attribute",
  105. content: content
  106. });
  107. }
  108. function invalidMatch(match) {
  109. this.selector.nodes.push({
  110. type: "invalid",
  111. value: match
  112. });
  113. }
  114. function irrelevantSpacingStartMatch(match) {
  115. this.selector.before = match;
  116. }
  117. function irrelevantSpacingEndMatch(match) {
  118. this.selector.after = match;
  119. }
  120. function nextSelectorMatch(match, before, after) {
  121. var newSelector = {
  122. type: "selector",
  123. nodes: []
  124. };
  125. if(before) {
  126. this.selector.after = before;
  127. }
  128. if(after) {
  129. newSelector.before = after;
  130. }
  131. this.root.nodes.push(newSelector);
  132. this.selector = newSelector;
  133. }
  134. function addToCurrent(match) {
  135. this.token.content += match;
  136. }
  137. function bracketStart(match) {
  138. this.token.content += match;
  139. this.brackets++;
  140. }
  141. function bracketEnd(match) {
  142. if(--this.brackets === 0) {
  143. return "selector";
  144. }
  145. this.token.content += match;
  146. }
  147. function getSelectors() {
  148. // The assignment here is split to preserve the property enumeration order.
  149. var selectors = {
  150. "/\\*([\\s\\S]*?)\\*/": commentMatch
  151. };
  152. // https://www.w3.org/TR/CSS21/syndata.html#characters
  153. // 4.1.3: identifiers (...) can contain only the characters [a-zA-Z0-9] and
  154. // ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_)
  155. //
  156. // 10ffff is the maximum allowed in current Unicode
  157. selectors[regexpu("\\.((?:\\\\.|[A-Za-z_\\-\\u{00a0}-\\u{10ffff}])(?:\\\\.|[A-Za-z_\\-0-9\\u{00a0}-\\u{10ffff}])*)", "u")] = typeMatch("class");
  158. selectors[regexpu("#((?:\\\\.|[A-Za-z_\\-\\u{00a0}-\\u{10ffff}])(?:\\\\.|[A-Za-z_\\-0-9\\u{00a0}-\\u{10ffff}])*)", "u")] = typeMatch("id");
  159. var selectorsSecondHalf = {
  160. ":(not|matches|has|local|global)\\((\\s*)": nestedPseudoClassStartMatch,
  161. ":((?:\\\\.|[A-Za-z_\\-0-9])+)\\(": pseudoClassStartMatch,
  162. ":((?:\\\\.|[A-Za-z_\\-0-9])+)": typeMatch("pseudo-class"),
  163. "::((?:\\\\.|[A-Za-z_\\-0-9])+)": typeMatch("pseudo-element"),
  164. "(\\*\\|)((?:\\\\.|[A-Za-z_\\-0-9])+)": elementMatch,
  165. "(\\*\\|)\\*": universalMatch,
  166. "((?:\\\\.|[A-Za-z_\\-0-9])*\\|)?\\*": universalMatch,
  167. "((?:\\\\.|[A-Za-z_\\-0-9])*\\|)?((?:\\\\.|[A-Za-z_\\-])(?:\\\\.|[A-Za-z_\\-0-9])*)": elementMatch,
  168. "\\[([^\\]]+)\\]": attributeMatch,
  169. "(\\s*)\\)": nestedEnd,
  170. "(\\s*)((?:\\|\\|)|(?:>>)|[>+~])(\\s*)": operatorMatch,
  171. "(\\s*),(\\s*)": nextSelectorMatch,
  172. "\\s+$": irrelevantSpacingEndMatch,
  173. "^\\s+": irrelevantSpacingStartMatch,
  174. "\\s+": spacingMatch,
  175. ".": invalidMatch
  176. };
  177. var selector;
  178. for (selector in selectorsSecondHalf) {
  179. if (Object.prototype.hasOwnProperty.call(selectorsSecondHalf, selector)) {
  180. selectors[selector] = selectorsSecondHalf[selector];
  181. }
  182. }
  183. return selectors;
  184. }
  185. var parser = new Parser({
  186. selector: getSelectors(),
  187. inBrackets: {
  188. "/\\*[\\s\\S]*?\\*/": addToCurrent,
  189. "\"([^\\\\\"]|\\\\.)*\"": addToCurrent,
  190. "'([^\\\\']|\\\\.)*'": addToCurrent,
  191. "[^()'\"/]+": addToCurrent,
  192. "\\(": bracketStart,
  193. "\\)": bracketEnd,
  194. ".": addToCurrent
  195. }
  196. });
  197. function parse(str) {
  198. var selectorNode = {
  199. type: "selector",
  200. nodes: []
  201. };
  202. var rootNode = {
  203. type: "selectors",
  204. nodes: [
  205. selectorNode
  206. ]
  207. };
  208. parser.parse("selector", str, {
  209. stack: [],
  210. root: rootNode,
  211. selector: selectorNode
  212. });
  213. return rootNode;
  214. }
  215. module.exports = parse;