@tbela99/css-parser
Version:
CSS parser, minifier and validator for node and the browser
513 lines (510 loc) • 22.3 kB
JavaScript
import { EnumToken } from '../types.js';
import { walkValues } from '../walk.js';
import { pseudoAliasMap } from '../../syntax/syntax.js';
import { splitRule } from '../minify.js';
import { renderValue } from '../../renderer/render.js';
import { TOKENS, funcLike, regMatchLinearGradient, regMatchRadialGradient } from '../../syntax/constants.js';
import { FeatureWalkMode } from './type.js';
import { ValidationSyntaxGroupEnum } from '../../validation/parser/typedef.js';
import { getSyntaxConfig } from '../../validation/config.js';
import { splitTokenList } from '../../validation/utils/list.js';
import { equalsIgnoreCase } from '../../parser/utils/text.js';
import { toDegrees } from '../../parser/utils/angle.js';
const config = getSyntaxConfig();
function replacePseudo(tokens) {
return tokens.map((raw) => raw.map((r) => {
if (r.includes("(")) {
const index = r.indexOf("(");
const name = r.slice(0, index) + "()";
if (name in pseudoAliasMap) {
return pseudoAliasMap[name] + r.slice(index);
}
return r;
}
return r in pseudoAliasMap && pseudoAliasMap[r] in config[ValidationSyntaxGroupEnum.Selectors]
? pseudoAliasMap[r]
: r;
}));
}
function replaceAstNodes(tokens, root) {
let result = false;
for (const { value, parent } of walkValues(tokens, root)) {
if (value.typ == EnumToken.MediaQueryConditionTokenType) {
const token = value.l.find((t) => t.typ == EnumToken.IdenTokenType);
if (token != null) {
if (token.val in pseudoAliasMap) {
token.val = pseudoAliasMap[token.val];
if ((equalsIgnoreCase(token.val, "min-resolution") ||
equalsIgnoreCase(token.val, "max-resolution")) &&
// ["min-resolution", "max-resolution"].includes((token as IdentToken).val) &&
value.r?.[0]?.typ == EnumToken.NumberTokenType) {
Object.assign(value.r?.[0], {
typ: EnumToken.ResolutionTokenType,
unit: "x",
});
result = true;
}
}
}
}
else if (value.typ == EnumToken.IdenTokenType ||
value.typ == EnumToken.PseudoClassFuncTokenType ||
value.typ == EnumToken.PseudoClassTokenType ||
value.typ == EnumToken.PseudoElementTokenType) {
let key = value.val +
(value.typ == EnumToken.PseudoClassFuncTokenType ? "()" : "");
if (key in pseudoAliasMap) {
const isPseudClass = pseudoAliasMap[key].startsWith("::");
value.val = pseudoAliasMap[key];
if (value.typ == EnumToken.IdenTokenType &&
["min-resolution", "max-resolution"].includes(value.val) &&
parent?.typ == EnumToken.MediaQueryConditionTokenType &&
parent.r?.[0]?.typ == EnumToken.NumberTokenType) {
Object.assign(parent.r?.[0], {
typ: EnumToken.ResolutionTokenType,
unit: "x",
});
}
else if (isPseudClass && value.typ == EnumToken.PseudoElementTokenType) {
// @ts-ignore
value.typ = EnumToken.PseudoClassTokenType;
}
result = true;
}
}
}
if (tokens.find((t) => t.typ == EnumToken.CommaTokenType) != null) {
const set = new Set();
const split = splitTokenList(tokens, [EnumToken.CommaTokenType]);
tokens.length = 0;
tokens.push(...split.reduce((acc, curr) => {
const str = curr.reduce((acc, curr) => acc + renderValue(curr), "");
if (set.has(str)) {
return acc;
}
set.add(str);
if (acc.length > 0) {
acc.push({
typ: EnumToken.CommaTokenType,
});
}
return acc.concat(curr);
}, []));
}
return result;
}
class ComputePrefixFeature {
get ordering() {
return 2;
}
get processMode() {
return FeatureWalkMode.Pre;
}
static register(options) {
if (options.removePrefix) {
// @ts-ignore
options.features.push(new ComputePrefixFeature(options));
}
}
run(node) {
if (node.typ == EnumToken.RuleNodeType) {
node.sel = replacePseudo(splitRule(node.sel)).reduce((acc, curr, index) => acc + (index > 0 ? "," : "") + curr.join(""), "");
if (node[TOKENS] != null) {
replaceAstNodes(node[TOKENS]);
}
}
else if (node.typ == EnumToken.DeclarationNodeType) {
if (node.nam.charAt(0) == "-") {
const match = node.nam.match(/^-([^-]+)-(.+)$/);
if (match != null) {
let nam = match[2];
if (!(nam in config.declarations)) {
if (node.nam in pseudoAliasMap) {
nam = pseudoAliasMap[node.nam];
}
}
if (nam in config.declarations) {
node.nam = nam;
}
}
}
let hasPrefix = false;
for (const { value } of walkValues(node.val)) {
if ((value.typ == EnumToken.IdenTokenType ||
(value.typ != EnumToken.ParensTokenType && funcLike.includes(value.typ))) &&
value.val.match(/^-([^-]+)-(.+)$/) != null) {
if (value.val.endsWith("-gradient")) {
if (regMatchLinearGradient.test(value.val)) {
this.webkitLinearToLinearGradient(value);
}
else if (regMatchRadialGradient.test(value.val)) {
this.webkitRadialToRadialGradient(value);
}
else if (equalsIgnoreCase(value.val, "-webkit-gradient")) {
this.webkitGradientToGradient(value);
}
// not supported yet
break;
}
hasPrefix = true;
break;
}
}
if (hasPrefix) {
const nodes = structuredClone(node.val);
for (const { value } of walkValues(nodes)) {
if (value.typ == EnumToken.IdenTokenType || funcLike.includes(value.typ)) {
const match = value.val.match(/^-([^-]+)-(.+)$/);
if (match != null) {
value.val = match[2];
}
}
}
node.val = nodes;
}
}
else if (node.typ == EnumToken.AtRuleNodeType || node.typ == EnumToken.KeyframesAtRuleNodeType) {
if (node.nam.startsWith("-")) {
const match = node.nam.match(/^-([^-]+)-(.+)$/);
if (match != null && "@" + match[2] in config.atRules) {
node.nam = match[2];
}
}
if (node.typ == EnumToken.AtRuleNodeType && node.val !== "") {
if (replaceAstNodes(node[TOKENS])) {
node.val = node[TOKENS].reduce((acc, curr) => acc + renderValue(curr), "");
}
}
}
return node;
}
/**
* Convert -webkit-linear-gradient to linear-gradient syntax
* @param tokens
* @returns
*/
webkitLinearToLinearGradient(token) {
let i;
let k;
let to;
let val;
let key;
const tokens = token.chi;
// conversion rules
// translation:
// - left -> to right
// - right -> to left
// - top -> to bottom
// - top right -> to bottom left
// - top left -> to bottom right
// - bottom -> to top
// - bottom right -> to top left
// - bottom left -> to top right
// https://drafts.csswg.org/css-images-3/#linear-gradients
// final angle:
// - to top -> 0deg
// - to right -> 90deg
// - to bottom -> 180deg
// - to left -> 270deg
for (i = 0; i < tokens.length; i++) {
if (tokens[i].typ == EnumToken.AngleTokenType ||
(tokens[i].typ == EnumToken.NumberTokenType && tokens[i].val == 0)) {
tokens[i].val =
(90 -
// @ts-expect-error
(tokens[i].typ == EnumToken.NumberTokenType
? tokens[i]
: toDegrees(tokens[i]).val)) %
360;
}
else if (tokens[i].typ == EnumToken.IdenTokenType) {
key = tokens[i].val.toLowerCase();
switch (key) {
case "right":
case "left":
tokens.splice(i, 1, { typ: EnumToken.IdenTokenType, val: "to" }, { typ: EnumToken.WhitespaceTokenType }, {
typ: EnumToken.IdenTokenType,
val: key == "right" ? "left" : "right",
});
i += 2;
break;
case "top":
case "bottom":
k = i + 1;
to = key == "top" ? "bottom" : "top";
while (k < tokens.length &&
(tokens[k].typ === EnumToken.WhitespaceTokenType ||
tokens[k].typ === EnumToken.CommentTokenType)) {
k++;
}
if (tokens[k]?.typ === EnumToken.IdenTokenType) {
val = "";
if (equalsIgnoreCase(tokens[k].val, "left")) {
val = "right";
}
if (equalsIgnoreCase(tokens[k].val, "right")) {
val = "left";
}
if (val !== "") {
tokens.splice(i, k - i + 1, { typ: EnumToken.IdenTokenType, val: "to" }, { typ: EnumToken.WhitespaceTokenType }, { typ: EnumToken.IdenTokenType, val: to }, { typ: EnumToken.WhitespaceTokenType }, { typ: EnumToken.IdenTokenType, val });
i += 4;
break;
}
}
tokens.splice(i, 1, { typ: EnumToken.IdenTokenType, val: "to" }, { typ: EnumToken.WhitespaceTokenType }, { typ: EnumToken.IdenTokenType, val: to });
i += 2;
break;
}
}
}
token.val = equalsIgnoreCase(token.val, "-webkit-repeating-linear-gradient")
? "repeating-linear-gradient"
: "linear-gradient";
}
/**
* Convert -webkit-gradient(linear) to linear-gradient or linear-gradient syntax
* @param token
* @returns
*/
webkitGradientToGradient(token) {
let i = 0;
let k;
let key = "";
let commaCount;
let type = "";
let tokens = token.chi.slice();
// while (
// i < tokens.length &&
// (tokens[i].typ === EnumToken.WhitespaceTokenType || tokens[i].typ === EnumToken.CommentTokenType)
// ) {
// i++;
// }
// if (i >= tokens.length || tokens[i].typ !== EnumToken.IdenTokenType) {
// return;
// }
// linear or radial
if (equalsIgnoreCase(tokens[i].val, "linear")) {
type = "linear-gradient";
i++;
}
// else {
// return;
// }
// while (
// i < tokens.length &&
// (tokens[i].typ === EnumToken.WhitespaceTokenType || tokens[i].typ === EnumToken.CommentTokenType)
// ) {
// i++;
// }
// if (tokens[i].typ !== EnumToken.CommaTokenType) {
// return;
// }
tokens.splice(0, i + 1);
commaCount = 0;
for (i = 0; i < tokens.length; i++) {
if (tokens[i].typ === EnumToken.CommaTokenType) {
commaCount++;
if (commaCount > 1) {
break;
}
}
if (tokens[i].typ === EnumToken.IdenTokenType) {
key += (key.length > 0 ? " " : "") + tokens[i].val;
}
}
// mapping keywords
// left top → left bottom to bottom
// left bottom → left top to top
// left top → right top to right
// right top → left top to left
// left top → right bottom to bottom right
// right top → left bottom to bottom left
// left bottom → right top to top right
// right bottom → left top to top left
const replacements = [];
if (key === "left top left bottom") {
replacements.push({ typ: EnumToken.IdenTokenType, val: "to" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "bottom" });
}
else if (key === "left bottom left top") {
replacements.push({ typ: EnumToken.IdenTokenType, val: "to" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "top" });
}
else if (key === "left top right top") {
replacements.push({ typ: EnumToken.IdenTokenType, val: "to" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "right" });
}
else if (key === "right top left top") {
replacements.push({ typ: EnumToken.IdenTokenType, val: "to" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "left" });
}
else if (key === "left top right bottom") {
replacements.push({ typ: EnumToken.IdenTokenType, val: "to" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "bottom" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "right" });
}
else if (key === "right top left bottom") {
replacements.push({ typ: EnumToken.IdenTokenType, val: "to" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "bottom" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "left" });
}
else if (key === "left bottom right top") {
replacements.push({ typ: EnumToken.IdenTokenType, val: "to" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "top" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "right" });
}
else if (key === "right bottom left top") {
replacements.push({ typ: EnumToken.IdenTokenType, val: "to" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "top" });
replacements.push({ typ: EnumToken.WhitespaceTokenType });
replacements.push({ typ: EnumToken.IdenTokenType, val: "left" });
}
tokens.splice(0, i, ...replacements);
let checkStop = true;
let checkStopIndex = replacements.length + 1;
let colorStop = [];
for (i = replacements.length + 1; i < tokens.length; i++) {
if (tokens[i].typ === EnumToken.CommaTokenType && checkStop) {
checkStopIndex = i;
continue;
}
if (tokens[i].typ === EnumToken.FunctionTokenType) {
if (equalsIgnoreCase(tokens[i].val, "to")) {
colorStop.push(tokens[checkStopIndex], ...tokens[i].chi);
tokens.splice(checkStopIndex, i - checkStopIndex + 1);
i = checkStopIndex;
checkStop = false;
}
else if (equalsIgnoreCase(tokens[i].val, "from")) {
k = tokens[i].chi.length + 1;
tokens.splice(i, 1, ...tokens[i].chi, { typ: EnumToken.WhitespaceTokenType }, { typ: EnumToken.PercentageTokenType, val: 0 });
i += k;
}
else if (equalsIgnoreCase(tokens[i].val, "color-stop")) {
k = tokens[i].chi.length - 1;
tokens.splice(i, 1,
// @ts-expect-error
...tokens[i].chi.reverse().map((t) => t.typ === EnumToken.CommaTokenType
? { typ: EnumToken.WhitespaceTokenType }
: t.typ === EnumToken.NumberTokenType
? {
typ: EnumToken.PercentageTokenType,
// @ts-expect-error
val: t.val * 100,
}
: t));
i += k;
}
}
}
if (colorStop.length > 0) {
tokens.push(...colorStop);
}
if (type !== "") {
token.val = type;
token.chi.length = 0;
token.chi.push(...tokens);
}
}
/**
* Convert -webkit-radial-gradient to radial-gradient syntax
* @param tokens
* @returns
*/
webkitRadialToRadialGradient(token) {
let i = 0;
const tokens = token.chi;
while (i < tokens.length &&
(tokens[i].typ === EnumToken.WhitespaceTokenType || tokens[i].typ === EnumToken.CommentTokenType)) {
i++;
}
const positions = [];
const form = [];
const size = [];
const colorStops = [];
if (tokens[i]?.typ === EnumToken.PercentageTokenType ||
(tokens[i]?.typ === EnumToken.NumberTokenType && 0 === tokens[i].val) ||
(tokens[i]?.typ === EnumToken.IdenTokenType &&
(equalsIgnoreCase(tokens[i].val, "center") ||
equalsIgnoreCase(tokens[i].val, "top") ||
equalsIgnoreCase(tokens[i].val, "bottom") ||
equalsIgnoreCase(tokens[i].val, "left") ||
equalsIgnoreCase(tokens[i].val, "right")))) {
do {
positions.push(tokens[i]);
i++;
} while (tokens[i]?.typ !== EnumToken.CommaTokenType);
if (tokens[i]?.typ === EnumToken.CommaTokenType) {
i++;
}
}
if (tokens[i]?.typ === EnumToken.IdenTokenType) {
if (equalsIgnoreCase(tokens[i].val, "closest-side") ||
equalsIgnoreCase(tokens[i].val, "closest-corner") ||
equalsIgnoreCase(tokens[i].val, "farthest-side") ||
equalsIgnoreCase(tokens[i].val, "farthest-corner")) {
size.push(tokens[i++]);
}
else if (equalsIgnoreCase(tokens[i].val, "circle") ||
equalsIgnoreCase(tokens[i].val, "ellipse")) {
form.push(tokens[i++]);
}
while (tokens[i]?.typ === EnumToken.WhitespaceTokenType || tokens[i]?.typ === EnumToken.CommentTokenType) {
i++;
}
if (tokens[i]?.typ === EnumToken.CommaTokenType) {
i++;
}
}
if (tokens[i]?.typ === EnumToken.IdenTokenType) {
if (equalsIgnoreCase(tokens[i].val, "closest-side") ||
equalsIgnoreCase(tokens[i].val, "closest-corner") ||
equalsIgnoreCase(tokens[i].val, "farthest-side") ||
equalsIgnoreCase(tokens[i].val, "farthest-corner")) {
size.push(tokens[i++]);
}
else if (equalsIgnoreCase(tokens[i].val, "circle") ||
equalsIgnoreCase(tokens[i].val, "ellipse")) {
form.push(tokens[i++]);
}
while (tokens[i]?.typ === EnumToken.WhitespaceTokenType || tokens[i]?.typ === EnumToken.CommentTokenType) {
i++;
}
if (tokens[i]?.typ === EnumToken.CommaTokenType) {
i++;
}
}
colorStops.push(...tokens.slice(i));
tokens.length = 0;
if (form.length > 0 || size.length > 0) {
if (form.length === 0) {
form.push({ typ: EnumToken.IdenTokenType, val: "ellipse" });
}
if (size.length > 0) {
form.push({ typ: EnumToken.WhitespaceTokenType });
form.push(...size);
}
if (positions.length > 0) {
form.push({ typ: EnumToken.WhitespaceTokenType }, { typ: EnumToken.IdenTokenType, val: "at" }, { typ: EnumToken.WhitespaceTokenType }, ...positions);
}
tokens.push(...form, { typ: EnumToken.CommaTokenType });
}
token.val = equalsIgnoreCase(token.val, "-webkit-repeating-radial-gradient")
? "repeating-radial-gradient"
: "radial-gradient";
tokens.push(...colorStops);
return tokens;
}
}
export { ComputePrefixFeature };