'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseMediaFeature = parseMediaFeature; exports.parseMediaQuery = parseMediaQuery; exports.parseMediaList = parseMediaList; var _Node = require('./nodes/Node'); var _Node2 = _interopRequireDefault(_Node); var _Container = require('./nodes/Container'); var _Container2 = _interopRequireDefault(_Container); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * Parses a media feature expression, e.g. `max-width: 10px`, `(color)` * * @param {string} string - the source expression string, can be inside parens * @param {Number} index - the index of `string` in the overall input * * @return {Array} an array of Nodes, the first element being a media feature, * the secont - its value (may be missing) */ function parseMediaFeature(string) { var index = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1]; var modesEntered = [{ mode: 'normal', character: null }]; var result = []; var lastModeIndex = 0; var mediaFeature = ''; var colon = null; var mediaFeatureValue = null; var indexLocal = index; var stringNormalized = string; // Strip trailing parens (if any), and correct the starting index if (string[0] === '(' && string[string.length - 1] === ')') { stringNormalized = string.substring(1, string.length - 1); indexLocal++; } for (var i = 0; i < stringNormalized.length; i++) { var character = stringNormalized[i]; // If entering/exiting a string if (character === '\'' || character === '"') { if (modesEntered[lastModeIndex].isCalculationEnabled === true) { modesEntered.push({ mode: 'string', isCalculationEnabled: false, character: character }); lastModeIndex++; } else if (modesEntered[lastModeIndex].mode === 'string' && modesEntered[lastModeIndex].character === character && stringNormalized[i - 1] !== '\\') { modesEntered.pop(); lastModeIndex--; } } // If entering/exiting interpolation if (character === '{') { modesEntered.push({ mode: 'interpolation', isCalculationEnabled: true }); lastModeIndex++; } else if (character === '}') { modesEntered.pop(); lastModeIndex--; } // If a : is met outside of a string, function call or interpolation, than // this : separates a media feature and a value if (modesEntered[lastModeIndex].mode === 'normal' && character === ':') { var mediaFeatureValueStr = stringNormalized.substring(i + 1); mediaFeatureValue = { type: 'value', before: /^(\s*)/.exec(mediaFeatureValueStr)[1], after: /(\s*)$/.exec(mediaFeatureValueStr)[1], value: mediaFeatureValueStr.trim() }; // +1 for the colon mediaFeatureValue.sourceIndex = mediaFeatureValue.before.length + i + 1 + indexLocal; colon = { type: 'colon', sourceIndex: i + indexLocal, after: mediaFeatureValue.before, value: ':' }; break; } mediaFeature += character; } // Forming a media feature node mediaFeature = { type: 'media-feature', before: /^(\s*)/.exec(mediaFeature)[1], after: /(\s*)$/.exec(mediaFeature)[1], value: mediaFeature.trim() }; mediaFeature.sourceIndex = mediaFeature.before.length + indexLocal; result.push(mediaFeature); if (colon !== null) { colon.before = mediaFeature.after; result.push(colon); } if (mediaFeatureValue !== null) { result.push(mediaFeatureValue); } return result; } /** * Parses a media query, e.g. `screen and (color)`, `only tv` * * @param {string} string - the source media query string * @param {Number} index - the index of `string` in the overall input * * @return {Array} an array of Nodes and Containers */ function parseMediaQuery(string) { var index = arguments.length <= 1 || arguments[1] === undefined ? 0 : arguments[1]; var result = []; // How many timies the parser entered parens/curly braces var localLevel = 0; // Has any keyword, media type, media feature expression or interpolation // ('element' hereafter) started var insideSomeValue = false; var node = void 0; function resetNode() { return { before: '', after: '', value: '' }; } node = resetNode(); for (var i = 0; i < string.length; i++) { var character = string[i]; // If not yet entered any element if (!insideSomeValue) { if (character.search(/\s/) !== -1) { // A whitespace // Don't form 'after' yet; will do it later node.before += character; } else { // Not a whitespace - entering an element // Expression start if (character === '(') { node.type = 'media-feature-expression'; localLevel++; } node.value = character; node.sourceIndex = index + i; insideSomeValue = true; } } else { // Already in the middle of some alement node.value += character; // Here parens just increase localLevel and don't trigger a start of // a media feature expression (since they can't be nested) // Interpolation start if (character === '{' || character === '(') { localLevel++; } // Interpolation/function call/media feature expression end if (character === ')' || character === '}') { localLevel--; } } // If exited all parens/curlies and the next symbol if (insideSomeValue && localLevel === 0 && (character === ')' || i === string.length - 1 || string[i + 1].search(/\s/) !== -1)) { if (['not', 'only', 'and'].indexOf(node.value) !== -1) { node.type = 'keyword'; } // if it's an expression, parse its contents if (node.type === 'media-feature-expression') { node.nodes = parseMediaFeature(node.value, node.sourceIndex); } result.push(Array.isArray(node.nodes) ? new _Container2.default(node) : new _Node2.default(node)); node = resetNode(); insideSomeValue = false; } } // Now process the result array - to specify undefined types of the nodes // and specify the `after` prop for (var _i = 0; _i < result.length; _i++) { node = result[_i]; if (_i > 0) { result[_i - 1].after = node.before; } // Node types. Might not be set because contains interpolation/function // calls or fully consists of them if (node.type === undefined) { if (_i > 0) { // only `and` can follow an expression if (result[_i - 1].type === 'media-feature-expression') { node.type = 'keyword'; continue; } // Anything after 'only|not' is a media type if (result[_i - 1].value === 'not' || result[_i - 1].value === 'only') { node.type = 'media-type'; continue; } // Anything after 'and' is an expression if (result[_i - 1].value === 'and') { node.type = 'media-feature-expression'; continue; } if (result[_i - 1].type === 'media-type') { // if it is the last element - it might be an expression // or 'and' depending on what is after it if (!result[_i + 1]) { node.type = 'media-feature-expression'; } else { node.type = result[_i + 1].type === 'media-feature-expression' ? 'keyword' : 'media-feature-expression'; } } } if (_i === 0) { // `screen`, `fn( ... )`, `#{ ... }`. Not an expression, since then // its type would have been set by now if (!result[_i + 1]) { node.type = 'media-type'; continue; } // `screen and` or `#{...} (max-width: 10px)` if (result[_i + 1] && (result[_i + 1].type === 'media-feature-expression' || result[_i + 1].type === 'keyword')) { node.type = 'media-type'; continue; } if (result[_i + 2]) { // `screen and (color) ...` if (result[_i + 2].type === 'media-feature-expression') { node.type = 'media-type'; result[_i + 1].type = 'keyword'; continue; } // `only screen and ...` if (result[_i + 2].type === 'keyword') { node.type = 'keyword'; result[_i + 1].type = 'media-type'; continue; } } if (result[_i + 3]) { // `screen and (color) ...` if (result[_i + 3].type === 'media-feature-expression') { node.type = 'keyword'; result[_i + 1].type = 'media-type'; result[_i + 2].type = 'keyword'; continue; } } } } } return result; } /** * Parses a media query list. Takes a possible `url()` at the start into * account, and divides the list into media queries that are parsed separately * * @param {string} string - the source media query list string * * @return {Array} an array of Nodes/Containers */ function parseMediaList(string) { var result = []; var interimIndex = 0; var levelLocal = 0; // Check for a `url(...)` part (if it is contents of an @import rule) var doesHaveUrl = /^(\s*)url\s*\(/.exec(string); if (doesHaveUrl !== null) { var i = doesHaveUrl[0].length; var parenthesesLv = 1; while (parenthesesLv > 0) { var character = string[i]; if (character === '(') { parenthesesLv++; } if (character === ')') { parenthesesLv--; } i++; } result.unshift(new _Node2.default({ type: 'url', value: string.substring(0, i).trim(), sourceIndex: doesHaveUrl[1].length, before: doesHaveUrl[1], after: /^(\s*)/.exec(string.substring(i))[1] })); interimIndex = i; } // Start processing the media query list for (var _i2 = interimIndex; _i2 < string.length; _i2++) { var _character = string[_i2]; // Dividing the media query list into comma-separated media queries // Only count commas that are outside of any parens // (i.e., not part of function call params list, etc.) if (_character === '(') { levelLocal++; } if (_character === ')') { levelLocal--; } if (levelLocal === 0 && _character === ',') { var _mediaQueryString = string.substring(interimIndex, _i2); var _spaceBefore = /^(\s*)/.exec(_mediaQueryString)[1]; result.push(new _Container2.default({ type: 'media-query', value: _mediaQueryString.trim(), sourceIndex: interimIndex + _spaceBefore.length, nodes: parseMediaQuery(_mediaQueryString, interimIndex), before: _spaceBefore, after: /(\s*)$/.exec(_mediaQueryString)[1] })); interimIndex = _i2 + 1; } } var mediaQueryString = string.substring(interimIndex); var spaceBefore = /^(\s*)/.exec(mediaQueryString)[1]; result.push(new _Container2.default({ type: 'media-query', value: mediaQueryString.trim(), sourceIndex: interimIndex + spaceBefore.length, nodes: parseMediaQuery(mediaQueryString, interimIndex), before: spaceBefore, after: /(\s*)$/.exec(mediaQueryString)[1] })); return result; }