generic.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. var tokenizerUtils = require('../tokenizer/utils');
  2. var findIdentifierEnd = tokenizerUtils.findIdentifierEnd;
  3. var findNumberEnd = tokenizerUtils.findNumberEnd;
  4. var findDecimalNumberEnd = tokenizerUtils.findDecimalNumberEnd;
  5. var isHex = tokenizerUtils.isHex;
  6. var tokenizerConst = require('../tokenizer/const');
  7. var SYMBOL_TYPE = tokenizerConst.SYMBOL_TYPE;
  8. var IDENTIFIER = tokenizerConst.TYPE.Identifier;
  9. var PLUSSIGN = tokenizerConst.TYPE.PlusSign;
  10. var HYPHENMINUS = tokenizerConst.TYPE.HyphenMinus;
  11. var NUMBERSIGN = tokenizerConst.TYPE.NumberSign;
  12. var PERCENTAGE = {
  13. '%': true
  14. };
  15. // https://www.w3.org/TR/css-values-3/#lengths
  16. var LENGTH = {
  17. // absolute length units
  18. 'px': true,
  19. 'mm': true,
  20. 'cm': true,
  21. 'in': true,
  22. 'pt': true,
  23. 'pc': true,
  24. 'q': true,
  25. // relative length units
  26. 'em': true,
  27. 'ex': true,
  28. 'ch': true,
  29. 'rem': true,
  30. // viewport-percentage lengths
  31. 'vh': true,
  32. 'vw': true,
  33. 'vmin': true,
  34. 'vmax': true,
  35. 'vm': true
  36. };
  37. var ANGLE = {
  38. 'deg': true,
  39. 'grad': true,
  40. 'rad': true,
  41. 'turn': true
  42. };
  43. var TIME = {
  44. 's': true,
  45. 'ms': true
  46. };
  47. var FREQUENCY = {
  48. 'hz': true,
  49. 'khz': true
  50. };
  51. // https://www.w3.org/TR/css-values-3/#resolution (https://drafts.csswg.org/css-values/#resolution)
  52. var RESOLUTION = {
  53. 'dpi': true,
  54. 'dpcm': true,
  55. 'dppx': true,
  56. 'x': true // https://github.com/w3c/csswg-drafts/issues/461
  57. };
  58. // https://drafts.csswg.org/css-grid/#fr-unit
  59. var FLEX = {
  60. 'fr': true
  61. };
  62. // https://www.w3.org/TR/css3-speech/#mixing-props-voice-volume
  63. var DECIBEL = {
  64. 'db': true
  65. };
  66. // https://www.w3.org/TR/css3-speech/#voice-props-voice-pitch
  67. var SEMITONES = {
  68. 'st': true
  69. };
  70. function consumeFunction(token, addTokenToMatch, getNextToken) {
  71. var length = 1;
  72. var cursor;
  73. do {
  74. cursor = getNextToken(length++);
  75. } while (cursor !== null && cursor.node !== token.node);
  76. if (cursor === null) {
  77. return false;
  78. }
  79. while (true) {
  80. // consume tokens until cursor
  81. if (addTokenToMatch() === cursor) {
  82. break;
  83. }
  84. }
  85. return true;
  86. }
  87. // TODO: implement
  88. // can be used wherever <length>, <frequency>, <angle>, <time>, <percentage>, <number>, or <integer> values are allowed
  89. // https://drafts.csswg.org/css-values/#calc-notation
  90. function calc(token, addTokenToMatch, getNextToken) {
  91. if (token === null) {
  92. return false;
  93. }
  94. var name = token.value.toLowerCase();
  95. if (name !== 'calc(' &&
  96. name !== '-moz-calc(' &&
  97. name !== '-webkit-calc(') {
  98. return false;
  99. }
  100. return consumeFunction(token, addTokenToMatch, getNextToken);
  101. }
  102. function attr(token, addTokenToMatch, getNextToken) {
  103. if (token === null || token.value.toLowerCase() !== 'attr(') {
  104. return false;
  105. }
  106. return consumeFunction(token, addTokenToMatch, getNextToken);
  107. }
  108. function expression(token, addTokenToMatch, getNextToken) {
  109. if (token === null || token.value.toLowerCase() !== 'expression(') {
  110. return false;
  111. }
  112. return consumeFunction(token, addTokenToMatch, getNextToken);
  113. }
  114. function url(token, addTokenToMatch, getNextToken) {
  115. if (token === null || token.value.toLowerCase() !== 'url(') {
  116. return false;
  117. }
  118. return consumeFunction(token, addTokenToMatch, getNextToken);
  119. }
  120. function idSelector(token, addTokenToMatch) {
  121. if (token === null) {
  122. return false;
  123. }
  124. if (token.value.charCodeAt(0) !== NUMBERSIGN) {
  125. return false;
  126. }
  127. if (consumeIdentifier(token.value, 1) !== token.value.length) {
  128. return false;
  129. }
  130. addTokenToMatch();
  131. return true;
  132. }
  133. function isNumber(str) {
  134. return /^[-+]?(\d+|\d*\.\d+)([eE][-+]?\d+)?$/.test(str);
  135. }
  136. function consumeNumber(str, allowFraction) {
  137. var code = str.charCodeAt(0);
  138. return findNumberEnd(str, code === PLUSSIGN || code === HYPHENMINUS ? 1 : 0, allowFraction);
  139. }
  140. function consumeIdentifier(str, offset) {
  141. var code = str.charCodeAt(offset);
  142. if (code < 0x80 && SYMBOL_TYPE[code] !== IDENTIFIER && code !== HYPHENMINUS) {
  143. return offset;
  144. }
  145. return findIdentifierEnd(str, offset + 1);
  146. }
  147. function astNode(type) {
  148. return function(token, addTokenToMatch) {
  149. if (token === null || token.node.type !== type) {
  150. return false;
  151. }
  152. addTokenToMatch();
  153. return true;
  154. };
  155. }
  156. function dimension(type) {
  157. return function(token, addTokenToMatch, getNextToken) {
  158. if (calc(token, addTokenToMatch, getNextToken)) {
  159. return true;
  160. }
  161. if (token === null) {
  162. return false;
  163. }
  164. var numberEnd = consumeNumber(token.value, true);
  165. if (numberEnd === 0) {
  166. return false;
  167. }
  168. if (type) {
  169. if (!type.hasOwnProperty(token.value.substr(numberEnd).toLowerCase())) {
  170. return false;
  171. }
  172. } else {
  173. var unitEnd = consumeIdentifier(token.value, numberEnd);
  174. if (unitEnd === numberEnd || unitEnd !== token.value.length) {
  175. return false;
  176. }
  177. }
  178. addTokenToMatch();
  179. return true;
  180. };
  181. }
  182. function zeroUnitlessDimension(type) {
  183. var isDimension = dimension(type);
  184. return function(token, addTokenToMatch, getNextToken) {
  185. if (isDimension(token, addTokenToMatch, getNextToken)) {
  186. return true;
  187. }
  188. if (token === null || Number(token.value) !== 0) {
  189. return false;
  190. }
  191. addTokenToMatch();
  192. return true;
  193. };
  194. }
  195. function number(token, addTokenToMatch, getNextToken) {
  196. if (calc(token, addTokenToMatch, getNextToken)) {
  197. return true;
  198. }
  199. if (token === null) {
  200. return false;
  201. }
  202. var numberEnd = consumeNumber(token.value, true);
  203. if (numberEnd !== token.value.length) {
  204. return false;
  205. }
  206. addTokenToMatch();
  207. return true;
  208. }
  209. function numberZeroOne(token, addTokenToMatch, getNextToken) {
  210. if (calc(token, addTokenToMatch, getNextToken)) {
  211. return true;
  212. }
  213. if (token === null || !isNumber(token.value)) {
  214. return false;
  215. }
  216. var value = Number(token.value);
  217. if (value < 0 || value > 1) {
  218. return false;
  219. }
  220. addTokenToMatch();
  221. return true;
  222. }
  223. function numberOneOrGreater(token, addTokenToMatch, getNextToken) {
  224. if (calc(token, addTokenToMatch, getNextToken)) {
  225. return true;
  226. }
  227. if (token === null || !isNumber(token.value)) {
  228. return false;
  229. }
  230. var value = Number(token.value);
  231. if (value < 1) {
  232. return false;
  233. }
  234. addTokenToMatch();
  235. return true;
  236. }
  237. // TODO: fail on 10e-2
  238. function integer(token, addTokenToMatch, getNextToken) {
  239. if (calc(token, addTokenToMatch, getNextToken)) {
  240. return true;
  241. }
  242. if (token === null) {
  243. return false;
  244. }
  245. var numberEnd = consumeNumber(token.value, false);
  246. if (numberEnd !== token.value.length) {
  247. return false;
  248. }
  249. addTokenToMatch();
  250. return true;
  251. }
  252. // TODO: fail on 10e-2
  253. function positiveInteger(token, addTokenToMatch, getNextToken) {
  254. if (calc(token, addTokenToMatch, getNextToken)) {
  255. return true;
  256. }
  257. if (token === null) {
  258. return false;
  259. }
  260. var numberEnd = findDecimalNumberEnd(token.value, 0);
  261. if (numberEnd !== token.value.length || token.value.charCodeAt(0) === HYPHENMINUS) {
  262. return false;
  263. }
  264. addTokenToMatch();
  265. return true;
  266. }
  267. function hexColor(token, addTokenToMatch) {
  268. if (token === null || token.value.charCodeAt(0) !== NUMBERSIGN) {
  269. return false;
  270. }
  271. var length = token.value.length - 1;
  272. // valid length is 3, 4, 6 and 8 (+1 for #)
  273. if (length !== 3 && length !== 4 && length !== 6 && length !== 8) {
  274. return false;
  275. }
  276. for (var i = 1; i < length; i++) {
  277. if (!isHex(token.value.charCodeAt(i))) {
  278. return false;
  279. }
  280. }
  281. addTokenToMatch();
  282. return true;
  283. }
  284. // https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident
  285. // https://drafts.csswg.org/css-values-4/#identifier-value
  286. function customIdent(token, addTokenToMatch) {
  287. if (token === null) {
  288. return false;
  289. }
  290. var identEnd = consumeIdentifier(token.value, 0);
  291. if (identEnd !== token.value.length) {
  292. return false;
  293. }
  294. var name = token.value.toLowerCase();
  295. // § 3.2. Author-defined Identifiers: the <custom-ident> type
  296. // The CSS-wide keywords are not valid <custom-ident>s
  297. if (name === 'unset' || name === 'initial' || name === 'inherit') {
  298. return false;
  299. }
  300. // The default keyword is reserved and is also not a valid <custom-ident>
  301. if (name === 'default') {
  302. return false;
  303. }
  304. // TODO: ignore property specific keywords (as described https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident)
  305. addTokenToMatch();
  306. return true;
  307. }
  308. module.exports = {
  309. 'angle': zeroUnitlessDimension(ANGLE),
  310. 'attr()': attr,
  311. 'custom-ident': customIdent,
  312. 'decibel': dimension(DECIBEL),
  313. 'dimension': dimension(),
  314. 'frequency': dimension(FREQUENCY),
  315. 'flex': dimension(FLEX),
  316. 'hex-color': hexColor,
  317. 'id-selector': idSelector, // element( <id-selector> )
  318. 'ident': astNode('Identifier'),
  319. 'integer': integer,
  320. 'length': zeroUnitlessDimension(LENGTH),
  321. 'number': number,
  322. 'number-zero-one': numberZeroOne,
  323. 'number-one-or-greater': numberOneOrGreater,
  324. 'percentage': dimension(PERCENTAGE),
  325. 'positive-integer': positiveInteger,
  326. 'resolution': dimension(RESOLUTION),
  327. 'semitones': dimension(SEMITONES),
  328. 'string': astNode('String'),
  329. 'time': dimension(TIME),
  330. 'unicode-range': astNode('UnicodeRange'),
  331. 'url': url,
  332. // old IE stuff
  333. 'progid': astNode('Raw'),
  334. 'expression': expression
  335. };