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.

585 lines
16 KiB

4 years ago
  1. var tokenizer = require('../tokenizer');
  2. var isIdentifierStart = tokenizer.isIdentifierStart;
  3. var isHexDigit = tokenizer.isHexDigit;
  4. var isDigit = tokenizer.isDigit;
  5. var cmpStr = tokenizer.cmpStr;
  6. var consumeNumber = tokenizer.consumeNumber;
  7. var TYPE = tokenizer.TYPE;
  8. var anPlusB = require('./generic-an-plus-b');
  9. var urange = require('./generic-urange');
  10. var cssWideKeywords = ['unset', 'initial', 'inherit'];
  11. var calcFunctionNames = ['calc(', '-moz-calc(', '-webkit-calc('];
  12. // https://www.w3.org/TR/css-values-3/#lengths
  13. var LENGTH = {
  14. // absolute length units
  15. 'px': true,
  16. 'mm': true,
  17. 'cm': true,
  18. 'in': true,
  19. 'pt': true,
  20. 'pc': true,
  21. 'q': true,
  22. // relative length units
  23. 'em': true,
  24. 'ex': true,
  25. 'ch': true,
  26. 'rem': true,
  27. // viewport-percentage lengths
  28. 'vh': true,
  29. 'vw': true,
  30. 'vmin': true,
  31. 'vmax': true,
  32. 'vm': true
  33. };
  34. var ANGLE = {
  35. 'deg': true,
  36. 'grad': true,
  37. 'rad': true,
  38. 'turn': true
  39. };
  40. var TIME = {
  41. 's': true,
  42. 'ms': true
  43. };
  44. var FREQUENCY = {
  45. 'hz': true,
  46. 'khz': true
  47. };
  48. // https://www.w3.org/TR/css-values-3/#resolution (https://drafts.csswg.org/css-values/#resolution)
  49. var RESOLUTION = {
  50. 'dpi': true,
  51. 'dpcm': true,
  52. 'dppx': true,
  53. 'x': true // https://github.com/w3c/csswg-drafts/issues/461
  54. };
  55. // https://drafts.csswg.org/css-grid/#fr-unit
  56. var FLEX = {
  57. 'fr': true
  58. };
  59. // https://www.w3.org/TR/css3-speech/#mixing-props-voice-volume
  60. var DECIBEL = {
  61. 'db': true
  62. };
  63. // https://www.w3.org/TR/css3-speech/#voice-props-voice-pitch
  64. var SEMITONES = {
  65. 'st': true
  66. };
  67. // safe char code getter
  68. function charCode(str, index) {
  69. return index < str.length ? str.charCodeAt(index) : 0;
  70. }
  71. function eqStr(actual, expected) {
  72. return cmpStr(actual, 0, actual.length, expected);
  73. }
  74. function eqStrAny(actual, expected) {
  75. for (var i = 0; i < expected.length; i++) {
  76. if (eqStr(actual, expected[i])) {
  77. return true;
  78. }
  79. }
  80. return false;
  81. }
  82. // IE postfix hack, i.e. 123\0 or 123px\9
  83. function isPostfixIeHack(str, offset) {
  84. if (offset !== str.length - 2) {
  85. return false;
  86. }
  87. return (
  88. str.charCodeAt(offset) === 0x005C && // U+005C REVERSE SOLIDUS (\)
  89. isDigit(str.charCodeAt(offset + 1))
  90. );
  91. }
  92. function outOfRange(opts, value, numEnd) {
  93. if (opts && opts.type === 'Range') {
  94. var num = Number(
  95. numEnd !== undefined && numEnd !== value.length
  96. ? value.substr(0, numEnd)
  97. : value
  98. );
  99. if (isNaN(num)) {
  100. return true;
  101. }
  102. if (opts.min !== null && num < opts.min) {
  103. return true;
  104. }
  105. if (opts.max !== null && num > opts.max) {
  106. return true;
  107. }
  108. }
  109. return false;
  110. }
  111. function consumeFunction(token, getNextToken) {
  112. var startIdx = token.index;
  113. var length = 0;
  114. // balanced token consuming
  115. do {
  116. length++;
  117. if (token.balance <= startIdx) {
  118. break;
  119. }
  120. } while (token = getNextToken(length));
  121. return length;
  122. }
  123. // TODO: implement
  124. // can be used wherever <length>, <frequency>, <angle>, <time>, <percentage>, <number>, or <integer> values are allowed
  125. // https://drafts.csswg.org/css-values/#calc-notation
  126. function calc(next) {
  127. return function(token, getNextToken, opts) {
  128. if (token === null) {
  129. return 0;
  130. }
  131. if (token.type === TYPE.Function && eqStrAny(token.value, calcFunctionNames)) {
  132. return consumeFunction(token, getNextToken);
  133. }
  134. return next(token, getNextToken, opts);
  135. };
  136. }
  137. function tokenType(expectedTokenType) {
  138. return function(token) {
  139. if (token === null || token.type !== expectedTokenType) {
  140. return 0;
  141. }
  142. return 1;
  143. };
  144. }
  145. function func(name) {
  146. name = name + '(';
  147. return function(token, getNextToken) {
  148. if (token !== null && eqStr(token.value, name)) {
  149. return consumeFunction(token, getNextToken);
  150. }
  151. return 0;
  152. };
  153. }
  154. // =========================
  155. // Complex types
  156. //
  157. // https://drafts.csswg.org/css-values-4/#custom-idents
  158. // 4.2. Author-defined Identifiers: the <custom-ident> type
  159. // Some properties accept arbitrary author-defined identifiers as a component value.
  160. // This generic data type is denoted by <custom-ident>, and represents any valid CSS identifier
  161. // that would not be misinterpreted as a pre-defined keyword in that property’s value definition.
  162. //
  163. // See also: https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident
  164. function customIdent(token) {
  165. if (token === null || token.type !== TYPE.Ident) {
  166. return 0;
  167. }
  168. var name = token.value.toLowerCase();
  169. // The CSS-wide keywords are not valid <custom-ident>s
  170. if (eqStrAny(name, cssWideKeywords)) {
  171. return 0;
  172. }
  173. // The default keyword is reserved and is also not a valid <custom-ident>
  174. if (eqStr(name, 'default')) {
  175. return 0;
  176. }
  177. // TODO: ignore property specific keywords (as described https://developer.mozilla.org/en-US/docs/Web/CSS/custom-ident)
  178. // Specifications using <custom-ident> must specify clearly what other keywords
  179. // are excluded from <custom-ident>, if any—for example by saying that any pre-defined keywords
  180. // in that property’s value definition are excluded. Excluded keywords are excluded
  181. // in all ASCII case permutations.
  182. return 1;
  183. }
  184. // https://drafts.csswg.org/css-variables/#typedef-custom-property-name
  185. // A custom property is any property whose name starts with two dashes (U+002D HYPHEN-MINUS), like --foo.
  186. // The <custom-property-name> production corresponds to this: it’s defined as any valid identifier
  187. // that starts with two dashes, except -- itself, which is reserved for future use by CSS.
  188. // NOTE: Current implementation treat `--` as a valid name since most (all?) major browsers treat it as valid.
  189. function customPropertyName(token) {
  190. // ... defined as any valid identifier
  191. if (token === null || token.type !== TYPE.Ident) {
  192. return 0;
  193. }
  194. // ... that starts with two dashes (U+002D HYPHEN-MINUS)
  195. if (charCode(token.value, 0) !== 0x002D || charCode(token.value, 1) !== 0x002D) {
  196. return 0;
  197. }
  198. return 1;
  199. }
  200. // https://drafts.csswg.org/css-color-4/#hex-notation
  201. // The syntax of a <hex-color> is a <hash-token> token whose value consists of 3, 4, 6, or 8 hexadecimal digits.
  202. // In other words, a hex color is written as a hash character, "#", followed by some number of digits 0-9 or
  203. // letters a-f (the case of the letters doesn’t matter - #00ff00 is identical to #00FF00).
  204. function hexColor(token) {
  205. if (token === null || token.type !== TYPE.Hash) {
  206. return 0;
  207. }
  208. var length = token.value.length;
  209. // valid values (length): #rgb (4), #rgba (5), #rrggbb (7), #rrggbbaa (9)
  210. if (length !== 4 && length !== 5 && length !== 7 && length !== 9) {
  211. return 0;
  212. }
  213. for (var i = 1; i < length; i++) {
  214. if (!isHexDigit(token.value.charCodeAt(i))) {
  215. return 0;
  216. }
  217. }
  218. return 1;
  219. }
  220. function idSelector(token) {
  221. if (token === null || token.type !== TYPE.Hash) {
  222. return 0;
  223. }
  224. if (!isIdentifierStart(charCode(token.value, 1), charCode(token.value, 2), charCode(token.value, 3))) {
  225. return 0;
  226. }
  227. return 1;
  228. }
  229. // https://drafts.csswg.org/css-syntax/#any-value
  230. // It represents the entirety of what a valid declaration can have as its value.
  231. function declarationValue(token, getNextToken) {
  232. if (!token) {
  233. return 0;
  234. }
  235. var length = 0;
  236. var level = 0;
  237. var startIdx = token.index;
  238. // The <declaration-value> production matches any sequence of one or more tokens,
  239. // so long as the sequence ...
  240. scan:
  241. do {
  242. switch (token.type) {
  243. // ... does not contain <bad-string-token>, <bad-url-token>,
  244. case TYPE.BadString:
  245. case TYPE.BadUrl:
  246. break scan;
  247. // ... unmatched <)-token>, <]-token>, or <}-token>,
  248. case TYPE.RightCurlyBracket:
  249. case TYPE.RightParenthesis:
  250. case TYPE.RightSquareBracket:
  251. if (token.balance > token.index || token.balance < startIdx) {
  252. break scan;
  253. }
  254. level--;
  255. break;
  256. // ... or top-level <semicolon-token> tokens
  257. case TYPE.Semicolon:
  258. if (level === 0) {
  259. break scan;
  260. }
  261. break;
  262. // ... or <delim-token> tokens with a value of "!"
  263. case TYPE.Delim:
  264. if (token.value === '!' && level === 0) {
  265. break scan;
  266. }
  267. break;
  268. case TYPE.Function:
  269. case TYPE.LeftParenthesis:
  270. case TYPE.LeftSquareBracket:
  271. case TYPE.LeftCurlyBracket:
  272. level++;
  273. break;
  274. }
  275. length++;
  276. // until balance closing
  277. if (token.balance <= startIdx) {
  278. break;
  279. }
  280. } while (token = getNextToken(length));
  281. return length;
  282. }
  283. // https://drafts.csswg.org/css-syntax/#any-value
  284. // The <any-value> production is identical to <declaration-value>, but also
  285. // allows top-level <semicolon-token> tokens and <delim-token> tokens
  286. // with a value of "!". It represents the entirety of what valid CSS can be in any context.
  287. function anyValue(token, getNextToken) {
  288. if (!token) {
  289. return 0;
  290. }
  291. var startIdx = token.index;
  292. var length = 0;
  293. // The <any-value> production matches any sequence of one or more tokens,
  294. // so long as the sequence ...
  295. scan:
  296. do {
  297. switch (token.type) {
  298. // ... does not contain <bad-string-token>, <bad-url-token>,
  299. case TYPE.BadString:
  300. case TYPE.BadUrl:
  301. break scan;
  302. // ... unmatched <)-token>, <]-token>, or <}-token>,
  303. case TYPE.RightCurlyBracket:
  304. case TYPE.RightParenthesis:
  305. case TYPE.RightSquareBracket:
  306. if (token.balance > token.index || token.balance < startIdx) {
  307. break scan;
  308. }
  309. break;
  310. }
  311. length++;
  312. // until balance closing
  313. if (token.balance <= startIdx) {
  314. break;
  315. }
  316. } while (token = getNextToken(length));
  317. return length;
  318. }
  319. // =========================
  320. // Dimensions
  321. //
  322. function dimension(type) {
  323. return function(token, getNextToken, opts) {
  324. if (token === null || token.type !== TYPE.Dimension) {
  325. return 0;
  326. }
  327. var numberEnd = consumeNumber(token.value, 0);
  328. // check unit
  329. if (type !== null) {
  330. // check for IE postfix hack, i.e. 123px\0 or 123px\9
  331. var reverseSolidusOffset = token.value.indexOf('\\', numberEnd);
  332. var unit = reverseSolidusOffset === -1 || !isPostfixIeHack(token.value, reverseSolidusOffset)
  333. ? token.value.substr(numberEnd)
  334. : token.value.substring(numberEnd, reverseSolidusOffset);
  335. if (type.hasOwnProperty(unit.toLowerCase()) === false) {
  336. return 0;
  337. }
  338. }
  339. // check range if specified
  340. if (outOfRange(opts, token.value, numberEnd)) {
  341. return 0;
  342. }
  343. return 1;
  344. };
  345. }
  346. // =========================
  347. // Percentage
  348. //
  349. // §5.5. Percentages: the <percentage> type
  350. // https://drafts.csswg.org/css-values-4/#percentages
  351. function percentage(token, getNextToken, opts) {
  352. // ... corresponds to the <percentage-token> production
  353. if (token === null || token.type !== TYPE.Percentage) {
  354. return 0;
  355. }
  356. // check range if specified
  357. if (outOfRange(opts, token.value, token.value.length - 1)) {
  358. return 0;
  359. }
  360. return 1;
  361. }
  362. // =========================
  363. // Numeric
  364. //
  365. // https://drafts.csswg.org/css-values-4/#numbers
  366. // The value <zero> represents a literal number with the value 0. Expressions that merely
  367. // evaluate to a <number> with the value 0 (for example, calc(0)) do not match <zero>;
  368. // only literal <number-token>s do.
  369. function zero(next) {
  370. if (typeof next !== 'function') {
  371. next = function() {
  372. return 0;
  373. };
  374. }
  375. return function(token, getNextToken, opts) {
  376. if (token !== null && token.type === TYPE.Number) {
  377. if (Number(token.value) === 0) {
  378. return 1;
  379. }
  380. }
  381. return next(token, getNextToken, opts);
  382. };
  383. }
  384. // § 5.3. Real Numbers: the <number> type
  385. // https://drafts.csswg.org/css-values-4/#numbers
  386. // Number values are denoted by <number>, and represent real numbers, possibly with a fractional component.
  387. // ... It corresponds to the <number-token> production
  388. function number(token, getNextToken, opts) {
  389. if (token === null) {
  390. return 0;
  391. }
  392. var numberEnd = consumeNumber(token.value, 0);
  393. var isNumber = numberEnd === token.value.length;
  394. if (!isNumber && !isPostfixIeHack(token.value, numberEnd)) {
  395. return 0;
  396. }
  397. // check range if specified
  398. if (outOfRange(opts, token.value, numberEnd)) {
  399. return 0;
  400. }
  401. return 1;
  402. }
  403. // §5.2. Integers: the <integer> type
  404. // https://drafts.csswg.org/css-values-4/#integers
  405. function integer(token, getNextToken, opts) {
  406. // ... corresponds to a subset of the <number-token> production
  407. if (token === null || token.type !== TYPE.Number) {
  408. return 0;
  409. }
  410. // The first digit of an integer may be immediately preceded by `-` or `+` to indicate the integer’s sign.
  411. var i = token.value.charCodeAt(0) === 0x002B || // U+002B PLUS SIGN (+)
  412. token.value.charCodeAt(0) === 0x002D ? 1 : 0; // U+002D HYPHEN-MINUS (-)
  413. // When written literally, an integer is one or more decimal digits 0 through 9 ...
  414. for (; i < token.value.length; i++) {
  415. if (!isDigit(token.value.charCodeAt(i))) {
  416. return 0;
  417. }
  418. }
  419. // check range if specified
  420. if (outOfRange(opts, token.value, i)) {
  421. return 0;
  422. }
  423. return 1;
  424. }
  425. module.exports = {
  426. // token types
  427. 'ident-token': tokenType(TYPE.Ident),
  428. 'function-token': tokenType(TYPE.Function),
  429. 'at-keyword-token': tokenType(TYPE.AtKeyword),
  430. 'hash-token': tokenType(TYPE.Hash),
  431. 'string-token': tokenType(TYPE.String),
  432. 'bad-string-token': tokenType(TYPE.BadString),
  433. 'url-token': tokenType(TYPE.Url),
  434. 'bad-url-token': tokenType(TYPE.BadUrl),
  435. 'delim-token': tokenType(TYPE.Delim),
  436. 'number-token': tokenType(TYPE.Number),
  437. 'percentage-token': tokenType(TYPE.Percentage),
  438. 'dimension-token': tokenType(TYPE.Dimension),
  439. 'whitespace-token': tokenType(TYPE.WhiteSpace),
  440. 'CDO-token': tokenType(TYPE.CDO),
  441. 'CDC-token': tokenType(TYPE.CDC),
  442. 'colon-token': tokenType(TYPE.Colon),
  443. 'semicolon-token': tokenType(TYPE.Semicolon),
  444. 'comma-token': tokenType(TYPE.Comma),
  445. '[-token': tokenType(TYPE.LeftSquareBracket),
  446. ']-token': tokenType(TYPE.RightSquareBracket),
  447. '(-token': tokenType(TYPE.LeftParenthesis),
  448. ')-token': tokenType(TYPE.RightParenthesis),
  449. '{-token': tokenType(TYPE.LeftCurlyBracket),
  450. '}-token': tokenType(TYPE.RightCurlyBracket),
  451. // token type aliases
  452. 'string': tokenType(TYPE.String),
  453. 'ident': tokenType(TYPE.Ident),
  454. // complex types
  455. 'custom-ident': customIdent,
  456. 'custom-property-name': customPropertyName,
  457. 'hex-color': hexColor,
  458. 'id-selector': idSelector, // element( <id-selector> )
  459. 'an-plus-b': anPlusB,
  460. 'urange': urange,
  461. 'declaration-value': declarationValue,
  462. 'any-value': anyValue,
  463. // dimensions
  464. 'dimension': calc(dimension(null)),
  465. 'angle': calc(dimension(ANGLE)),
  466. 'decibel': calc(dimension(DECIBEL)),
  467. 'frequency': calc(dimension(FREQUENCY)),
  468. 'flex': calc(dimension(FLEX)),
  469. 'length': calc(zero(dimension(LENGTH))),
  470. 'resolution': calc(dimension(RESOLUTION)),
  471. 'semitones': calc(dimension(SEMITONES)),
  472. 'time': calc(dimension(TIME)),
  473. // percentage
  474. 'percentage': calc(percentage),
  475. // numeric
  476. 'zero': zero(),
  477. 'number': calc(number),
  478. 'integer': calc(integer),
  479. // old IE stuff
  480. '-ms-legacy-expression': func('expression')
  481. };