blob: fc92025731a24d8d3efa5cdcee9e4eed4982b579 [file] [log] [blame]
'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;
}