@tbela99/css-parser
Version:
CSS parser, minifier and validator for node and the browser
356 lines (354 loc) • 10.9 kB
JavaScript
/**
* Options for the walk function
*/
var WalkerOptionEnum;
(function (WalkerOptionEnum) {
/**
* ignore the current node and its children
*/
WalkerOptionEnum[WalkerOptionEnum["Ignore"] = 1] = "Ignore";
/**
* stop walking the tree
*/
WalkerOptionEnum[WalkerOptionEnum["Stop"] = 2] = "Stop";
/**
* ignore the current node and process its children
*/
WalkerOptionEnum[WalkerOptionEnum["Children"] = 4] = "Children";
/**
* ignore the current node children
*/
WalkerOptionEnum[WalkerOptionEnum["IgnoreChildren"] = 8] = "IgnoreChildren";
})(WalkerOptionEnum || (WalkerOptionEnum = {}));
/**
* Event types for the walkValues function
*/
var WalkerEvent;
(function (WalkerEvent) {
/**
* enter node
*/
WalkerEvent[WalkerEvent["Enter"] = 1] = "Enter";
/**
* leave node
*/
WalkerEvent[WalkerEvent["Leave"] = 2] = "Leave";
})(WalkerEvent || (WalkerEvent = {}));
/**
* Walk ast nodes
* @param node initial node
* @param filter control the walk process
* @param reverse walk in reverse order
*
* ```ts
*
* import {walk} from '@tbela99/css-parser';
*
* const css = `
* body { color: color(from var(--base-color) display-p3 r calc(g + 0.24) calc(b + 0.15)); }
*
* html,
* body {
* line-height: 1.474;
* }
*
* .ruler {
*
* height: 10px;
* }
* `;
*
* for (const {node, parent, root} of walk(ast)) {
*
* // do something with node
* }
* ```
*
* Using a {@link filter} function to control the ast traversal. the filter function returns a value of type {@link WalkerOption}.
*
* ```ts
* import {EnumToken, transform, walk, WalkerOptionEnum} from '@tbela99/css-parser';
*
* const css = `
* body { color: color(from var(--base-color) display-p3 r calc(g + 0.24) calc(b + 0.15)); }
*
* html,
* body {
* line-height: 1.474;
* }
*
* .ruler {
*
* height: 10px;
* }
* `;
*
* function filter(node) {
*
* if (node.typ == EnumToken.AstRule && node.sel.includes('html')) {
*
* // skip the children of the current node
* return WalkerOptionEnum.IgnoreChildren;
* }
* }
*
* const result = await transform(css);
* for (const {node} of walk(result.ast, filter)) {
*
* console.error([EnumToken[node.typ]]);
* }
*
* // [ "StyleSheetNodeType" ]
* // [ "RuleNodeType" ]
* // [ "DeclarationNodeType" ]
* // [ "RuleNodeType" ]
* // [ "DeclarationNodeType" ]
* // [ "RuleNodeType" ]
* // [ "DeclarationNodeType" ]
* ```
*/
function* walk(node, filter, reverse) {
const parents = [node];
const root = node;
const map = new Map();
let isNumeric = false;
let i = 0;
while ((node = parents[i++])) {
let option = null;
if (filter != null) {
option = filter(node);
isNumeric = typeof option == "number";
if (isNumeric) {
if (option & WalkerOptionEnum.Ignore) {
continue;
}
if (option & WalkerOptionEnum.Stop) {
break;
}
}
}
if (!isNumeric || (option & WalkerOptionEnum.Children) === 0) {
// @ts-ignore
yield {
node,
parent: map.get(node),
root,
// @ts-expect-error
parents: function* () {
let parent = map.get(node);
while (parent != null) {
yield parent;
parent = map.get(parent);
}
},
};
}
if ("chi" in node && (!isNumeric || (option & WalkerOptionEnum.IgnoreChildren) === 0)) {
parents.splice(i, 0, ...node.chi[reverse ? "toReversed" : "slice"]());
for (const child of node.chi) {
map.set(child, node);
}
}
}
}
/**
* Walk ast node value tokens
* @param values
* @param root
* @param filter
* @param reverse
*
* Example:
*
* ```ts
*
* import {AstDeclaration, EnumToken, transform, walkValues} from '@tbela99/css-parser';
*
* const css = `
* body { color: color(from var(--base-color) display-p3 r calc(g + 0.24) calc(b + 0.15)); }
* `;
*
* const result = await transform(css);
* const declaration = result.ast.chi[0].chi[0] as AstDeclaration;
*
* // walk the node attribute's tokens in reverse order
* for (const {value} of walkValues(declaration.val, null, null,true)) {
*
* console.error([EnumToken[value.typ], value.val]);
* }
*
* // [ "Color", "color" ]
* // [ "FunctionTokenType", "calc" ]
* // [ "Number", 0.15 ]
* // [ "Add", undefined ]
* // [ "Iden", "b" ]
* // [ "Whitespace", undefined ]
* // [ "FunctionTokenType", "calc" ]
* // [ "Number", 0.24 ]
* // [ "Add", undefined ]
* // [ "Iden", "g" ]
* // [ "Whitespace", undefined ]
* // [ "Iden", "r" ]
* // [ "Whitespace", undefined ]
* // [ "Iden", "display-p3" ]
* // [ "Whitespace", undefined ]
* // [ "FunctionTokenType", "var" ]
* // [ "DashedIden", "--base-color" ]
* // [ "Whitespace", undefined ]
* // [ "Iden", "from" ]
* ```
*/
function* walkValues(values, root = null, filter, reverse) {
const stack = values.slice();
const map = new Map();
const used = new Set();
let previous = null;
if (filter != null && typeof filter == "function") {
filter = {
event: WalkerEvent.Enter,
fn: filter,
};
}
else if (filter == null) {
filter = {
event: WalkerEvent.Enter,
};
}
let isNumeric = false;
let value;
let option;
let node;
// const parents: Token[] = [];
const eventType = filter.event ?? WalkerEvent.Enter;
while (stack.length > 0) {
value = reverse ? stack.pop() : stack.shift();
option = null;
node = map.get(value) ?? null;
if (used.has(value)) {
continue;
}
used.add(value);
// parents.length = 0;
// while (node != null) {
// parents.push(node);
// node = map.get(node) ?? null;
// }
if (filter.fn != null && eventType & WalkerEvent.Enter) {
const isValid = filter.type == null ||
value.typ == filter.type ||
(Array.isArray(filter.type) && filter.type.includes(value.typ)) ||
(typeof filter.type == "function" && filter.type(value));
if (isValid) {
option = filter.fn(value, map.get(value) ?? root, WalkerEvent.Enter,
// @ts-expect-error
function* () {
// @ts-expect-error
let parent = map.get(node);
while (parent != null) {
yield parent;
parent = map.get(parent);
}
});
isNumeric = typeof option == "number";
if (isNumeric && option & WalkerOptionEnum.Stop) {
return;
}
if (isNumeric && option & WalkerOptionEnum.Ignore) {
continue;
}
// @ts-ignore
if (option != null && typeof option == "object" && ("typ" in option || Array.isArray(option))) {
const op = Array.isArray(option) ? option : [option];
for (const o of op) {
map.set(o, map.get(value) ?? root);
}
stack[reverse ? "push" : "unshift"](...op);
}
}
}
yield {
value,
parent: map.get(value) ?? root,
previousValue: previous,
nextValue: stack[0] ?? null,
// @ts-ignore
root: root ?? null,
// @ts-expect-error
parents: function* () {
// @ts-expect-error
let result = map.get(node) ?? root;
let next;
do {
yield result;
next = map.get(result) ?? root;
if (next == result) {
break;
}
result = next;
} while (result != null);
},
};
if (!isNumeric || (option & WalkerOptionEnum.IgnoreChildren) === 0) {
if ("chi" in value) {
const sliced = value.chi.slice();
for (const child of sliced) {
map.set(child, value);
}
stack[reverse ? "push" : "unshift"](...sliced);
}
else {
const values = [];
if ("l" in value && value.l != null) {
// @ts-ignore
values.push(value.l);
// @ts-ignore
map.set(value.l, value);
}
if ("op" in value && typeof value.op == "object") {
// @ts-ignore
values.push(value.op);
// @ts-ignore
map.set(value.op, value);
}
if ("r" in value && value.r != null) {
if (Array.isArray(value.r)) {
for (const r of value.r) {
// @ts-ignore
values.push(r);
// @ts-ignore
map.set(r, value);
}
}
else {
// @ts-ignore
values.push(value.r);
// @ts-ignore
map.set(value.r, value);
}
}
if (values.length > 0) {
stack[reverse ? "push" : "unshift"](...values);
}
}
}
if (eventType & WalkerEvent.Leave && filter.fn != null) {
const isValid = filter.type == null ||
value.typ == filter.type ||
(Array.isArray(filter.type) && filter.type.includes(value.typ)) ||
(typeof filter.type == "function" && filter.type(value));
if (isValid) {
option = filter.fn(value, map.get(value), WalkerEvent.Leave);
// @ts-ignore
if (option != null && ("typ" in option || Array.isArray(option))) {
const op = Array.isArray(option) ? option : [option];
for (const o of op) {
map.set(o, map.get(value) ?? root);
}
stack[reverse ? "push" : "unshift"](...op);
}
}
}
previous = value;
}
}
export { WalkerEvent, WalkerOptionEnum, walk, walkValues };