parsers.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. 'use strict';
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.parseMediaFeature = parseMediaFeature;
  6. exports.parseMediaQuery = parseMediaQuery;
  7. exports.parseMediaList = parseMediaList;
  8. var _Node = require('./nodes/Node');
  9. var _Node2 = _interopRequireDefault(_Node);
  10. var _Container = require('./nodes/Container');
  11. var _Container2 = _interopRequireDefault(_Container);
  12. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  13. /**
  14. * Parses a media feature expression, e.g. `max-width: 10px`, `(color)`
  15. *
  16. * @param {string} string - the source expression string, can be inside parens
  17. * @param {Number} index - the index of `string` in the overall input
  18. *
  19. * @return {Array} an array of Nodes, the first element being a media feature,
  20. * the secont - its value (may be missing)
  21. */
  22. function parseMediaFeature(string) {
  23. var index = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1];
  24. var modesEntered = [{
  25. mode: 'normal',
  26. character: null
  27. }];
  28. var result = [];
  29. var lastModeIndex = 0;
  30. var mediaFeature = '';
  31. var colon = null;
  32. var mediaFeatureValue = null;
  33. var indexLocal = index;
  34. var stringNormalized = string;
  35. // Strip trailing parens (if any), and correct the starting index
  36. if (string[0] === '(' && string[string.length - 1] === ')') {
  37. stringNormalized = string.substring(1, string.length - 1);
  38. indexLocal++;
  39. }
  40. for (var i = 0; i < stringNormalized.length; i++) {
  41. var character = stringNormalized[i];
  42. // If entering/exiting a string
  43. if (character === '\'' || character === '"') {
  44. if (modesEntered[lastModeIndex].isCalculationEnabled === true) {
  45. modesEntered.push({
  46. mode: 'string',
  47. isCalculationEnabled: false,
  48. character: character
  49. });
  50. lastModeIndex++;
  51. } else if (modesEntered[lastModeIndex].mode === 'string' && modesEntered[lastModeIndex].character === character && stringNormalized[i - 1] !== '\\') {
  52. modesEntered.pop();
  53. lastModeIndex--;
  54. }
  55. }
  56. // If entering/exiting interpolation
  57. if (character === '{') {
  58. modesEntered.push({
  59. mode: 'interpolation',
  60. isCalculationEnabled: true
  61. });
  62. lastModeIndex++;
  63. } else if (character === '}') {
  64. modesEntered.pop();
  65. lastModeIndex--;
  66. }
  67. // If a : is met outside of a string, function call or interpolation, than
  68. // this : separates a media feature and a value
  69. if (modesEntered[lastModeIndex].mode === 'normal' && character === ':') {
  70. var mediaFeatureValueStr = stringNormalized.substring(i + 1);
  71. mediaFeatureValue = {
  72. type: 'value',
  73. before: /^(\s*)/.exec(mediaFeatureValueStr)[1],
  74. after: /(\s*)$/.exec(mediaFeatureValueStr)[1],
  75. value: mediaFeatureValueStr.trim()
  76. };
  77. // +1 for the colon
  78. mediaFeatureValue.sourceIndex = mediaFeatureValue.before.length + i + 1 + indexLocal;
  79. colon = {
  80. type: 'colon',
  81. sourceIndex: i + indexLocal,
  82. after: mediaFeatureValue.before,
  83. value: ':' };
  84. break;
  85. }
  86. mediaFeature += character;
  87. }
  88. // Forming a media feature node
  89. mediaFeature = {
  90. type: 'media-feature',
  91. before: /^(\s*)/.exec(mediaFeature)[1],
  92. after: /(\s*)$/.exec(mediaFeature)[1],
  93. value: mediaFeature.trim()
  94. };
  95. mediaFeature.sourceIndex = mediaFeature.before.length + indexLocal;
  96. result.push(mediaFeature);
  97. if (colon !== null) {
  98. colon.before = mediaFeature.after;
  99. result.push(colon);
  100. }
  101. if (mediaFeatureValue !== null) {
  102. result.push(mediaFeatureValue);
  103. }
  104. return result;
  105. }
  106. /**
  107. * Parses a media query, e.g. `screen and (color)`, `only tv`
  108. *
  109. * @param {string} string - the source media query string
  110. * @param {Number} index - the index of `string` in the overall input
  111. *
  112. * @return {Array} an array of Nodes and Containers
  113. */
  114. function parseMediaQuery(string) {
  115. var index = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1];
  116. var result = [];
  117. // How many timies the parser entered parens/curly braces
  118. var localLevel = 0;
  119. // Has any keyword, media type, media feature expression or interpolation
  120. // ('element' hereafter) started
  121. var insideSomeValue = false;
  122. var node = void 0;
  123. function resetNode() {
  124. return {
  125. before: '',
  126. after: '',
  127. value: ''
  128. };
  129. }
  130. node = resetNode();
  131. for (var i = 0; i < string.length; i++) {
  132. var character = string[i];
  133. // If not yet entered any element
  134. if (!insideSomeValue) {
  135. if (character.search(/\s/) !== -1) {
  136. // A whitespace
  137. // Don't form 'after' yet; will do it later
  138. node.before += character;
  139. } else {
  140. // Not a whitespace - entering an element
  141. // Expression start
  142. if (character === '(') {
  143. node.type = 'media-feature-expression';
  144. localLevel++;
  145. }
  146. node.value = character;
  147. node.sourceIndex = index + i;
  148. insideSomeValue = true;
  149. }
  150. } else {
  151. // Already in the middle of some alement
  152. node.value += character;
  153. // Here parens just increase localLevel and don't trigger a start of
  154. // a media feature expression (since they can't be nested)
  155. // Interpolation start
  156. if (character === '{' || character === '(') {
  157. localLevel++;
  158. }
  159. // Interpolation/function call/media feature expression end
  160. if (character === ')' || character === '}') {
  161. localLevel--;
  162. }
  163. }
  164. // If exited all parens/curlies and the next symbol
  165. if (insideSomeValue && localLevel === 0 && (character === ')' || i === string.length - 1 || string[i + 1].search(/\s/) !== -1)) {
  166. if (['not', 'only', 'and'].indexOf(node.value) !== -1) {
  167. node.type = 'keyword';
  168. }
  169. // if it's an expression, parse its contents
  170. if (node.type === 'media-feature-expression') {
  171. node.nodes = parseMediaFeature(node.value, node.sourceIndex);
  172. }
  173. result.push(Array.isArray(node.nodes) ? new _Container2.default(node) : new _Node2.default(node));
  174. node = resetNode();
  175. insideSomeValue = false;
  176. }
  177. }
  178. // Now process the result array - to specify undefined types of the nodes
  179. // and specify the `after` prop
  180. for (var _i = 0; _i < result.length; _i++) {
  181. node = result[_i];
  182. if (_i > 0) {
  183. result[_i - 1].after = node.before;
  184. }
  185. // Node types. Might not be set because contains interpolation/function
  186. // calls or fully consists of them
  187. if (node.type === undefined) {
  188. if (_i > 0) {
  189. // only `and` can follow an expression
  190. if (result[_i - 1].type === 'media-feature-expression') {
  191. node.type = 'keyword';
  192. continue;
  193. }
  194. // Anything after 'only|not' is a media type
  195. if (result[_i - 1].value === 'not' || result[_i - 1].value === 'only') {
  196. node.type = 'media-type';
  197. continue;
  198. }
  199. // Anything after 'and' is an expression
  200. if (result[_i - 1].value === 'and') {
  201. node.type = 'media-feature-expression';
  202. continue;
  203. }
  204. if (result[_i - 1].type === 'media-type') {
  205. // if it is the last element - it might be an expression
  206. // or 'and' depending on what is after it
  207. if (!result[_i + 1]) {
  208. node.type = 'media-feature-expression';
  209. } else {
  210. node.type = result[_i + 1].type === 'media-feature-expression' ? 'keyword' : 'media-feature-expression';
  211. }
  212. }
  213. }
  214. if (_i === 0) {
  215. // `screen`, `fn( ... )`, `#{ ... }`. Not an expression, since then
  216. // its type would have been set by now
  217. if (!result[_i + 1]) {
  218. node.type = 'media-type';
  219. continue;
  220. }
  221. // `screen and` or `#{...} (max-width: 10px)`
  222. if (result[_i + 1] && (result[_i + 1].type === 'media-feature-expression' || result[_i + 1].type === 'keyword')) {
  223. node.type = 'media-type';
  224. continue;
  225. }
  226. if (result[_i + 2]) {
  227. // `screen and (color) ...`
  228. if (result[_i + 2].type === 'media-feature-expression') {
  229. node.type = 'media-type';
  230. result[_i + 1].type = 'keyword';
  231. continue;
  232. }
  233. // `only screen and ...`
  234. if (result[_i + 2].type === 'keyword') {
  235. node.type = 'keyword';
  236. result[_i + 1].type = 'media-type';
  237. continue;
  238. }
  239. }
  240. if (result[_i + 3]) {
  241. // `screen and (color) ...`
  242. if (result[_i + 3].type === 'media-feature-expression') {
  243. node.type = 'keyword';
  244. result[_i + 1].type = 'media-type';
  245. result[_i + 2].type = 'keyword';
  246. continue;
  247. }
  248. }
  249. }
  250. }
  251. }
  252. return result;
  253. }
  254. /**
  255. * Parses a media query list. Takes a possible `url()` at the start into
  256. * account, and divides the list into media queries that are parsed separately
  257. *
  258. * @param {string} string - the source media query list string
  259. *
  260. * @return {Array} an array of Nodes/Containers
  261. */
  262. function parseMediaList(string) {
  263. var result = [];
  264. var interimIndex = 0;
  265. var levelLocal = 0;
  266. // Check for a `url(...)` part (if it is contents of an @import rule)
  267. var doesHaveUrl = /^(\s*)url\s*\(/.exec(string);
  268. if (doesHaveUrl !== null) {
  269. var i = doesHaveUrl[0].length;
  270. var parenthesesLv = 1;
  271. while (parenthesesLv > 0) {
  272. var character = string[i];
  273. if (character === '(') {
  274. parenthesesLv++;
  275. }
  276. if (character === ')') {
  277. parenthesesLv--;
  278. }
  279. i++;
  280. }
  281. result.unshift(new _Node2.default({
  282. type: 'url',
  283. value: string.substring(0, i).trim(),
  284. sourceIndex: doesHaveUrl[1].length,
  285. before: doesHaveUrl[1],
  286. after: /^(\s*)/.exec(string.substring(i))[1]
  287. }));
  288. interimIndex = i;
  289. }
  290. // Start processing the media query list
  291. for (var _i2 = interimIndex; _i2 < string.length; _i2++) {
  292. var _character = string[_i2];
  293. // Dividing the media query list into comma-separated media queries
  294. // Only count commas that are outside of any parens
  295. // (i.e., not part of function call params list, etc.)
  296. if (_character === '(') {
  297. levelLocal++;
  298. }
  299. if (_character === ')') {
  300. levelLocal--;
  301. }
  302. if (levelLocal === 0 && _character === ',') {
  303. var _mediaQueryString = string.substring(interimIndex, _i2);
  304. var _spaceBefore = /^(\s*)/.exec(_mediaQueryString)[1];
  305. result.push(new _Container2.default({
  306. type: 'media-query',
  307. value: _mediaQueryString.trim(),
  308. sourceIndex: interimIndex + _spaceBefore.length,
  309. nodes: parseMediaQuery(_mediaQueryString, interimIndex),
  310. before: _spaceBefore,
  311. after: /(\s*)$/.exec(_mediaQueryString)[1]
  312. }));
  313. interimIndex = _i2 + 1;
  314. }
  315. }
  316. var mediaQueryString = string.substring(interimIndex);
  317. var spaceBefore = /^(\s*)/.exec(mediaQueryString)[1];
  318. result.push(new _Container2.default({
  319. type: 'media-query',
  320. value: mediaQueryString.trim(),
  321. sourceIndex: interimIndex + spaceBefore.length,
  322. nodes: parseMediaQuery(mediaQueryString, interimIndex),
  323. before: spaceBefore,
  324. after: /(\s*)$/.exec(mediaQueryString)[1]
  325. }));
  326. return result;
  327. }