@tbela99/css-parser
Version:
CSS parser, minifier and validator for node and the browser
605 lines (602 loc) • 36.1 kB
JavaScript
import { EnumToken } from '../../ast/types.js';
import { evaluate } from '../../ast/math/expression.js';
import { gcd } from '../../ast/math/math.js';
import { tokensfuncDefMap, mediaTypes, LOC, mFLT, mFGT } from '../../syntax/constants.js';
import { trimArray, matchAllSyntaxes, createValidationContext, getMFInfo, isMFValue } from '../../validation/match.js';
import { ValidationSyntaxGroupEnum, MediaFeatureType } from '../../validation/parser/typedef.js';
import { getParsedSyntax } from '../../validation/config.js';
// https://drafts.csswg.org/mediaqueries/#media-descriptor-table:~:text=It%20is%20invalid%20to%20mix%20and%20and%20or%20and%20not%20at%20the%20same%20%E2%80%9Clevel%E2%80%9D%20of%20a%20media%20query%2E
// 'or' is not allowed at the same level as 'and' and 'not'
// 'or' is not allowed outside of parentheses
// https://drafts.csswg.org/mediaqueries/#error-handling
// error recovery
function parseMediaqueryList(stream, options) {
let matchCount = 0;
const errors = [];
let hasErrors = false;
const parts = stream.reduce((acc, t) => {
if (t.typ === EnumToken.CommaTokenType && matchCount === 0) {
acc.push([]);
}
else {
acc[acc.length - 1].push(t);
if (t.typ === EnumToken.StartParensTokenType || tokensfuncDefMap.has(t.typ)) {
matchCount++;
}
else if (t.typ === EnumToken.EndParensTokenType) {
if (matchCount > 0) {
matchCount--;
}
}
}
return acc;
}, [[]]);
{
for (const stream of parts.slice()) {
let success = true;
let i;
let currentScope = new Set();
const scopes = [currentScope];
const stack = [];
const tokens = [];
let expectAndOrComma = false;
i = 0;
while (i < stream.length &&
(stream[i]?.typ === EnumToken.WhitespaceTokenType || stream[i]?.typ === EnumToken.CommentTokenType)) {
tokens.push(stream[i]);
i++;
}
// [not | only] <media-type> [ and <media-condition-without-or> ]?
if (i < stream.length) {
if (stream[i].typ === EnumToken.IdenTokenType) {
const val = stream[i].val.toLowerCase();
// if ("not" === val || "only" === val) {
// tokens.push(stream[i]);
// stack.push(stream[i]);
// i++;
// while (
// i < stream.length &&
// (stream[i]?.typ === EnumToken.WhitespaceTokenType ||
// stream[i]?.typ === EnumToken.CommentTokenType)
// ) {
// tokens.push(stream[i]);
// i++;
// }
// if (
// stream[i]?.typ !== EnumToken.IdenTokenType ||
// !mediaTypes.includes((stream[i] as IdentToken).val.toLowerCase())
// ) {
// errors.push({
// action: "drop",
// node: stream[i],
// message: `expecting '<media-type>' at ${stream[i]?.[LOC]?.src}:${stream[i]?.[LOC]?.sta.lin}:${stream[i]?.[LOC]?.sta.col}`,
// });
// continue;
// }
// const index: number = tokens.indexOf(stack[stack.length - 1]);
// tokens[index][LOC] = { ...tokens[index][LOC]!, end: stream[i][LOC]!.end };
// tokens[index] = Object.assign(tokens[index], {
// typ: val === "not" ? EnumToken.NotTokenType : EnumToken.OnlyTokenType,
// val: stream[i],
// });
// tokens.length = index + 1;
// i++;
// // expect end of stream | and | or | not
// while (
// i < stream.length &&
// (stream[i]?.typ === EnumToken.WhitespaceTokenType ||
// stream[i]?.typ === EnumToken.CommentTokenType)
// ) {
// tokens.push(stream[i]);
// i++;
// }
// if (i < stream.length) {
// if (stream[i].typ === EnumToken.AndTokenType) {
// tokens.push(Object.assign(stream[i], { typ: EnumToken.AndTokenType }));
// stack.push(stream[i]);
// i++;
// } else if (stream[i].typ === EnumToken.OrTokenType) {
// tokens.push(Object.assign(stream[i], { typ: EnumToken.OrTokenType }));
// stack.push(stream[i]);
// i++;
// } else {
// success = false;
// errors.push({
// action: "drop",
// node: stream[i],
// message: `expecting 'and' or 'or' at ${stream[i]?.[LOC]?.src}:${stream[i]?.[LOC]?.sta.lin}:${stream[i]?.[LOC]?.sta.col}`,
// });
// }
// }
// } else
if (mediaTypes.includes(val)) {
tokens.push(stream[i]);
i++;
}
else {
success = false;
errors.push({
action: "drop",
message: `expecting '<media-type>' at ${stream[i]?.[LOC]?.src}:${stream[i]?.[LOC]?.sta.lin}:${stream[i]?.[LOC]?.sta.col}`,
node: stream[i],
location: stream[i][LOC],
});
}
}
else if (stream[i].typ !== EnumToken.StartParensTokenType) {
success = false;
errors.push({
action: "drop",
message: `expecting '(' at ${stream[i]?.[LOC]?.src}:${stream[i]?.[LOC]?.sta.lin}:${stream[i]?.[LOC]?.sta.col}`,
node: stream[i],
location: stream[i][LOC],
});
}
}
for (; i < stream.length; i++) {
tokens.push(stream[i]);
if (stream[i].typ === EnumToken.WhitespaceTokenType || stream[i].typ === EnumToken.CommentTokenType) {
continue;
}
if (expectAndOrComma) {
if (stream[i].typ === EnumToken.IdenTokenType) {
stream[i].val.toLowerCase();
}
// if (!valid) {
// success = false;
// errors.push({
// action: "drop",
// node: stream[i],
// message: `expecting <and>, <or> or comma at ${stream[i]?.[LOC]?.src}:${stream[i]?.[LOC]?.sta.lin}:${stream[i]?.[LOC]?.sta.col}`,
// });
// break;
// }
expectAndOrComma = false;
}
if (stream[i].typ === EnumToken.StartParensTokenType || tokensfuncDefMap.has(stream[i].typ)) {
scopes.push((currentScope = new Set()));
stack.push(stream[i]);
continue;
}
switch (stream[i].typ) {
// case EnumToken.LiteralTokenType:
// if ((stream[i] as LiteralToken).val === "<") {
// stack.push(Object.assign(stream[i], { typ: EnumToken.LtTokenType }));
// }
// if ((stream[i] as LiteralToken).val === ">") {
// stack.push(Object.assign(stream[i], { typ: EnumToken.GtTokenType }));
// }
// break;
case EnumToken.ColonTokenType:
case EnumToken.LtTokenType:
case EnumToken.LteTokenType:
case EnumToken.GtTokenType:
case EnumToken.GteTokenType:
case EnumToken.DelimTokenType:
stack.push(stream[i]);
break;
case EnumToken.IdenTokenType:
{
const val = stream[i].val.toLowerCase();
// if (val === "not" || val === "only") {
// Object.assign(stream[i], {
// typ: val === "not" ? EnumToken.NotTokenType : EnumToken.OnlyTokenType,
// });
// stack.push(stream[i]);
// } else
if (val === "and" || val === "or") {
Object.assign(stream[i], {
typ: val === "and" ? EnumToken.AndTokenType : EnumToken.OrTokenType,
});
if (val === "or" && scopes.length <= 1) {
success = false;
errors.push({
action: "drop",
node: stream[i],
message: `<or> is not allowed outside of parentheses ${stream[i]?.[LOC]?.src}:${stream[i]?.[LOC]?.sta.lin}:${stream[i]?.[LOC]?.sta.col}`,
});
break;
}
if (currentScope.has(val === "or" ? EnumToken.AndTokenType : EnumToken.OrTokenType)) {
success = false;
errors.push({
action: "drop",
node: stream[i],
message: `cannot mix <and> and <or> at the same level at ${stream[i]?.[LOC]?.src}:${stream[i]?.[LOC]?.sta.lin}:${stream[i]?.[LOC]?.sta.col}`,
});
}
currentScope.add(stream[i].typ);
stack.push(stream[i]);
}
}
break;
case EnumToken.EndParensTokenType:
if (tokensfuncDefMap.has(stack.at(-1)?.typ)) {
const index = tokens.indexOf(stack.at(-1));
tokens[index][LOC] = { ...tokens[index][LOC], end: stream[i][LOC].end };
Object.assign(tokens[index], {
typ: tokensfuncDefMap.get(stack.at(-1)?.typ),
chi: trimArray(tokens.slice(index + 1, tokens.length - 1)),
});
tokens.length = index + 1;
const result = matchAllSyntaxes(getParsedSyntax(ValidationSyntaxGroupEnum.Syntaxes, tokens[index].val + "()")?.[0]?.chi, createValidationContext(tokens[index].chi), options);
stack.pop();
scopes.pop();
currentScope = scopes.at(-1);
if (!result.success) {
errors.push(...result.errors);
success = false;
}
break;
}
// feature
if (mFLT.has(stack.at(-1)?.typ) || mFGT.has(stack.at(-1)?.typ)) {
// <mf-lt> | <mf-gt>
const index = tokens.indexOf(stack.at(-1));
const prevToken = stack[stack.length - 2];
if (mFLT.has(prevToken?.typ) || mFGT.has(prevToken?.typ)) {
// if (stack[stack.length - 3]?.typ !== EnumToken.StartParensTokenType) {
// success = false;
// errors.push({
// action: "drop",
// node: stream[i],
// message: `unmatched '(' at ${stream[i]?.[LOC]?.src}:${stream[i]?.[LOC]?.sta.lin}:${stream[i]?.[LOC]?.sta.col}`,
// });
// break;
// }
// if (!mFLT.has(stack.at(-1)?.typ) && mFLT.has(prevToken?.typ)) {
// success = false;
// errors.push({
// action: "drop",
// node: stack.at(-1),
// message: `expected <mf-lt> at ${stack.at(-1)?.[LOC]?.src}:${stack.at(-1)?.[LOC]?.sta.lin}:${stack.at(-1)?.[LOC]?.sta.col}`,
// });
// break;
// } else if (!mFGT.has(stack.at(-1)?.typ) && mFGT.has(prevToken?.typ)) {
// success = false;
// errors.push({
// action: "drop",
// node: stream[i],
// message: `expected <mf-gt> at ${stack.at(-1)?.[LOC]?.src}:${stack.at(-1)?.[LOC]?.sta.lin}:${stack.at(-1)?.[LOC]?.sta.col}`,
// });
// break;
// }
// const index: number = tokens.indexOf(stack.at(-1)!);
// <mf-lt> | <mf-name>
const index2 = tokens.indexOf(prevToken);
// '('
const index3 = tokens.indexOf(stack.at(-3));
const left = trimArray(tokens.slice(index3 + 1, index2));
const right = trimArray(tokens.slice(index + 1, tokens.length - 1));
const names = trimArray(tokens.slice(index2 + 1, index));
const filteredNames = names.filter((n) => n.typ !== EnumToken.WhitespaceTokenType && n.typ !== EnumToken.CommentTokenType);
// if (filteredNames.length !== 1 || filteredNames[0].typ !== EnumToken.IdenTokenType) {
// success = false;
// errors.push({
// action: "drop",
// node: stream[i],
// message: `expected <mf-name>> at ${filteredNames[0]?.[LOC]?.src}:${filteredNames[0]?.[LOC]?.sta.lin}:${filteredNames[0]?.[LOC]?.sta.col}`,
// });
// break;
// }
const name = filteredNames[0].val;
const mfInfo = getMFInfo(name);
for (const val of [left, right]) {
if (options.computeCalcExpression) {
if (mfInfo == null ||
(mfInfo.type != MediaFeatureType.KeywordType &&
mfInfo.type != MediaFeatureType.StringType)) {
for (let l = 0; l < val.length; l++) {
if (val[l].typ === EnumToken.MathFunctionTokenType &&
val[l].val === "calc") {
const value = evaluate([val[l]]);
if (value.length == 1) {
// if (mfInfo?.type === MediaFeatureType.RatioType) {
// if (
// value[0].typ !== EnumToken.NumberTokenType ||
// !(
// ((value[0] as NumberToken).val as FractionToken)
// ?.typ === EnumToken.FractionTokenType
// )
// ) {
// continue;
// }
// }
value[0][LOC] = val[l][LOC];
val[l] = value[0];
}
}
}
}
}
}
isMFValue(name, left, true);
// if (isValidMFValue.valid && !isValidMFValue.success) {
// success = false;
// errors.push({
// action: "drop",
// node: left[0] ?? prevToken,
// message: `${isValidMFValue.isValueAllowed === false ? "invalid <mf-name>" : "expected <mf-value>"} at ${(left[0] ?? prevToken)?.[LOC]?.src}:${(left[0] ?? prevToken)?.[LOC]?.sta.lin}:${(left[0] ?? prevToken)?.[LOC]?.sta.col}`,
// });
// break;
// }
isMFValue(name, right, true);
// if (isValidMFValue.valid && !isValidMFValue.success) {
// success = false;
// errors.push({
// action: "drop",
// node: right[0] ?? stream[i],
// location: (right[0] ?? stream[i])?.[LOC],
// message: `${isValidMFValue.isValueAllowed === false ? "invalid <mf-name>" : "expected <mf-value>"} at ${(right[0] ?? stream[i])?.[LOC]?.src}:${(right[0] ?? stream[i])?.[LOC]?.sta.lin}:${(left[0] ?? stream[i])?.[LOC]?.sta.col}`,
// });
// break;
// }
for (const val of [left, right]) {
if (mfInfo?.type === MediaFeatureType.RatioType) {
const filteredValues = val.filter((n) => n.typ !== EnumToken.WhitespaceTokenType &&
n.typ !== EnumToken.CommentTokenType);
if (filteredValues.length === 3) {
if (options.computeCalcExpression) {
const div = gcd(filteredValues[0].val, filteredValues[2].val);
if (div > 1) {
filteredValues[0].val =
filteredValues[0].val / div;
filteredValues[2].val =
filteredValues[2].val / div;
}
}
val.splice(0, val.length, ...filteredValues);
}
}
}
tokens.splice(index3 + 1, tokens.length - index3 - 2, {
typ: EnumToken.MediaRangeQueryTokenType,
l: left,
val: filteredNames,
op1: prevToken,
op2: stack.at(-1),
r: right,
[LOC]: { ...left[0][LOC], end: right.at(-1)[LOC].end },
});
stack.pop();
stack.pop();
}
// else if (stack[stack.length - 2]?.typ !== EnumToken.StartParensTokenType) {
// success = false;
// errors.push({
// action: "drop",
// node: stream[i],
// location: stream[i]?.[LOC],
// message: `expected '(' at ${stream[i]?.[LOC]?.src}:${stream[i]?.[LOC]?.sta.lin}:${stream[i]?.[LOC]?.sta.col}`,
// });
// break;
// }
}
if (stack.length > 0 &&
(mFGT.has(stack.at(-1)?.typ) ||
mFLT.has(stack.at(-1)?.typ) ||
stack.at(-1)?.typ === EnumToken.DelimTokenType ||
stack.at(-1)?.typ === EnumToken.ColonTokenType)) {
// if (stack[stack.length - 2]?.typ !== EnumToken.StartParensTokenType) {
// success = false;
// errors.push({
// action: "drop",
// node: stream[i],
// location: stream[i]?.[LOC],
// message: `expected '(' at ${(stack[stack.length - 2] ?? tokens[0])?.[LOC]?.src}:${(stack[stack.length - 2] ?? tokens[0])?.[LOC]?.sta.lin}:${(stack[stack.length - 2] ?? tokens[0])?.[LOC]?.sta.col}`,
// });
// break;
// }
const index2 = tokens.indexOf(stack.at(-1));
const index3 = tokens.indexOf(stack.at(-2));
let names = trimArray(tokens.slice(index3 + 1, index2));
let values = trimArray(tokens.slice(index2 + 1, tokens.length - 1));
// if (stack.at(-1)?.typ !== EnumToken.ColonTokenType) {
// const filteredNames = names.filter(
// (n) =>
// n.typ !== EnumToken.WhitespaceTokenType && n.typ !== EnumToken.CommentTokenType,
// );
// if (
// filteredNames.length !== 1 ||
// (filteredNames[0].typ !== EnumToken.IdenTokenType &&
// filteredNames[0].typ !== EnumToken.DashedIdenTokenType)
// ) {
// swapped = true;
// }
// }
const filteredNames = (names).filter((n) => n.typ !== EnumToken.WhitespaceTokenType && n.typ !== EnumToken.CommentTokenType);
// if (
// filteredNames.length !== 1 ||
// (filteredNames[0].typ !== EnumToken.IdenTokenType &&
// filteredNames[0].typ !== EnumToken.DashedIdenTokenType)
// ) {
// success = false;
// errors.push({
// action: "drop",
// node: names[0] ?? stack.at(-1),
// location: names[0]?.[LOC],
// message: `expected <mf-name> at ${(names[0] ?? stack.at(-1))?.[LOC]?.src}:${(names[0] ?? stack.at(-1))?.[LOC]?.sta.lin}:${(names[0] ?? stack.at(-1))?.[LOC]?.sta.col}`,
// });
// break;
// }
const name = filteredNames[0].val;
// if (!isMFName(name)) {
// success = false;
// errors.push({
// action: "drop",
// node: names[0] ?? stack.at(-1),
// location: names[0]?.[LOC] ?? stack.at(-1)?.[LOC],
// message: `expected <mf-name> at ${names[0]?.[LOC]?.src}:${names[0]?.[LOC]?.sta.lin}:${names[0]?.[LOC]?.sta.col}`,
// });
// break;
// }
const mfInfo = getMFInfo(name);
if (options.computeCalcExpression) {
if (mfInfo == null ||
(mfInfo.type != MediaFeatureType.KeywordType &&
mfInfo.type != MediaFeatureType.StringType)) {
const val = values;
for (let l = 0; l < val.length; l++) {
if (val[l].typ === EnumToken.MathFunctionTokenType &&
val[l].val === "calc") {
const value = evaluate([val[l]]);
if (value.length == 1) {
// if (mfInfo?.type === MediaFeatureType.RatioType) {
// if (
// value[0].typ !== EnumToken.NumberTokenType ||
// !(
// ((value[0] as NumberToken).val as FractionToken)?.typ ===
// EnumToken.FractionTokenType
// )
// ) {
// continue;
// }
// }
value[0][LOC] = val[l][LOC];
val[l] = value[0];
}
}
}
}
}
const mfValue = isMFValue(name, values, mFGT.has(stack.at(-1)?.typ) ||
mFLT.has(stack.at(-1)?.typ) ||
stack.at(-1)?.typ === EnumToken.DelimTokenType);
if (!mfValue.success) {
success = false;
const arr = values;
errors.push({
action: "drop",
node: arr[0],
location: arr[0]?.[LOC],
message: `${mfValue.isValueAllowed === false ? "invalid <mf-name>" : "expected <mf-value>"} at ${arr[0]?.[LOC]?.src}:${arr[0]?.[LOC]?.sta.lin}:${arr[0]?.[LOC]?.sta.col}`,
});
break;
}
if (mfInfo?.type === MediaFeatureType.RatioType) {
const val = values;
const filteredValues = val.filter((n) => n.typ !== EnumToken.WhitespaceTokenType && n.typ !== EnumToken.CommentTokenType);
if (filteredValues.length === 3) {
if (options.computeCalcExpression) {
const div = gcd(filteredValues[0].val, filteredValues[2].val);
if (div > 1) {
filteredValues[0].val =
filteredValues[0].val / div;
filteredValues[2].val =
filteredValues[2].val / div;
}
}
val.splice(0, val.length, ...filteredValues);
}
}
tokens.splice(index3 + 1, tokens.length - index3 - 2, {
typ: EnumToken.MediaQueryConditionTokenType,
l: names,
op: stack.pop(),
r: values,
// @ts-expect-error
[LOC]: { ...names[0][LOC], end: values.at(-1)[LOC].end },
});
}
if (stack.length === 0) {
success = false;
errors.push({
action: "drop",
node: stream[i],
location: stream[i]?.[LOC],
message: `unmatched ')' at ${stream[i]?.[LOC]?.src}:${stream[i]?.[LOC]?.sta.lin}:${stream[i]?.[LOC]?.sta.col}`,
});
break;
}
{
const index = tokens.indexOf(stack.at(-1));
tokens[index] = {
typ: EnumToken.ParensTokenType,
chi: tokens.slice(index + 1, tokens.length - 1),
// @ts-expect-error
[LOC]: { ...tokens[index][LOC], end: stream[i][LOC].end },
};
tokens.length = index + 1;
scopes.pop();
currentScope = scopes.at(-1);
stack.pop();
// if (
// stack.at(-1)?.typ === EnumToken.NotTokenType ||
// stack.at(-1)?.typ === EnumToken.OnlyTokenType
// ) {
// const index = tokens.indexOf(stack.at(-1)!);
// const slice = trimArray(tokens.slice(index + 1));
// tokens[index] = {
// typ: EnumToken.MediaQueryUnaryFeatureTokenType,
// l: stack.pop()!,
// r: slice,
// // @ts-expect-error
// [LOC]: { ...tokens[index][LOC]!, end: slice.at(-1)![LOC]!.end } as Location,
// };
// tokens.length = index + 1;
// }
if (stack.at(-1)?.typ === EnumToken.AndTokenType ||
stack.at(-1)?.typ === EnumToken.OrTokenType) {
const index = tokens.indexOf(stack.at(-1));
let l = index - 1;
while (l > 0 &&
(tokens[l].typ === EnumToken.WhitespaceTokenType ||
tokens[l].typ === EnumToken.CommentTokenType)) {
l--;
}
const left = trimArray(tokens.slice(l, index));
const right = trimArray(tokens.slice(index + 1));
tokens[l] = {
typ: EnumToken.MediaQueryConditionTokenType,
op: stack.pop(),
l: left,
r: right,
[LOC]: { ...left[0][LOC], end: right.at(-1)[LOC].end },
};
tokens.length = l + 1;
expectAndOrComma = true;
}
}
break;
}
if (!success) {
break;
}
}
// if (success && stack.length > 0) {
// success = false;
// errors.push({
// action: "drop",
// node: stack.at(-1),
// location: stack.at(-1)?.[LOC],
// message: `unmatched token '${EnumToken[stack.at(-1)?.typ]}' at ${stack.at(-1)?.[LOC]?.src}:${stack.at(-1)?.[LOC]?.sta.lin}:${stack.at(-1)?.[LOC]?.sta.col}`,
// });
// }
if (!success) {
hasErrors = true;
parts.splice(parts.indexOf(stream), 1);
}
stream.length = 0;
stream.push(...trimArray(tokens));
}
}
stream.length = 0;
stream.push(...parts
.filter((p) => p.length > 0 && p[0].typ !== EnumToken.InvalidMediaQueryTokenType)
.reduce((acc, b) => {
if (acc.length > 0) {
acc.push({ typ: EnumToken.CommaTokenType });
}
acc.push(...b);
return acc;
}, []));
return {
success: !hasErrors ||
stream.filter((t) => t.typ !== EnumToken.WhitespaceTokenType &&
t.typ !== EnumToken.CommentTokenType &&
t.typ !== EnumToken.InvalidMediaQueryTokenType &&
t.typ !== EnumToken.CommaTokenType).length !== 0,
errors,
};
}
export { parseMediaqueryList };