@styn/tree
Version:
159 lines (137 loc) • 4.43 kB
text/typescript
// Hey, hi! You can see an example of tree in the test file.
export type Keyword = string;
export type Selector = string;
export type Declarations = { [property: string]: number | string | Declarations };
export type Rule = {
type: "rule";
selector: Selector;
declarations: Declarations;
};
export type AtRule = {
type: "at-rule";
keyword: Keyword;
values?: string[];
declarations?: Declarations;
rules?: Rule[];
};
export type StynRule = Rule | AtRule;
export type StynTree = {
rules: StynRule[];
meta: {
[k: string]: any;
[k: number]: any;
};
};
const i = (content: string, count: number) => `${" ".repeat(count)}${content}`; // indentation
const linewrap = (content: string) =>
content[content.length - 1] === "\n" ? content : `${content}\n`;
const block = (content: string, _i = 0) => {
const open = `{\n`;
const close = i("}", _i);
return `${open}${linewrap(content)}${close}\n`;
};
const normalizeProp = (prop: string) =>
[...prop].map((l) => (l.toLowerCase() === l ? l : `-${l.toLowerCase()}`)).join("");
const join = (arr: any[], fn: Function = (x: any) => x, ...args: any[]) => {
return arr
.map((item) => fn(item, ...args))
.map(linewrap)
.join("");
};
export const stringify = (tree: StynTree) => {
const stringifyDeclarations = (declarations: Declarations, _i = 0) => {
return block(
Object.keys(declarations)
.map((property) => {
const prop = normalizeProp(property);
const value = declarations[property];
if (typeof value === "object") return undefined;
return linewrap(i(`${prop}: ${value};`, _i + 2));
})
.filter(Boolean)
.join(""),
_i
);
};
const stringifyRule = (rule: Rule, _i = 0) => {
const declarations = stringifyDeclarations(rule.declarations, _i);
return linewrap(i(`${rule.selector} ${declarations}`, _i));
};
const stringifyAtRule = (atRule: AtRule, _i = 0) => {
const keyword = atRule.keyword;
const values = atRule.values ? ` ${atRule.values.join(" ")}` : "";
if (atRule.declarations) {
const declarations = stringifyDeclarations(atRule.declarations, _i);
return linewrap(i(`${keyword}${values} ${declarations}`, _i));
} else if (atRule.rules) {
const rules = atRule.rules.map((rule) => stringifyRule(rule, _i + 2)).join("");
return linewrap(i(`${keyword}${values} ${block(rules, _i)}`, _i));
} else {
return linewrap(i(`${keyword}${values};`, _i));
}
};
const stringifyStynRule = (rule: StynRule) => {
if (rule.type === "at-rule") {
return stringifyAtRule(rule as AtRule);
} else if (rule.type === "rule") {
return stringifyRule(rule as Rule);
}
return "";
};
return join(tree.rules.map(stringifyStynRule).filter(Boolean));
};
export const parse = (object: { [k: string]: any }): StynTree => {
const rules: StynRule[] = [];
for (const prop in object) {
const value = object[prop];
if (prop.startsWith("@")) {
// Handle at-rules
const atRule: AtRule = {
type: "at-rule",
keyword: prop,
};
if (prop.startsWith("@font-face")) {
// Handle at-rules with declarations
atRule.declarations = value;
} else if (typeof value === "object") {
// Handle at-rules with selectors (rules)
atRule.rules = [];
for (const selector in value) {
atRule.rules.push({
type: "rule",
selector,
declarations: value[selector],
});
}
} else if (typeof value === "string") {
atRule.values = [value];
}
rules.push(atRule);
} else {
// Handle normal rules
const rule: Rule = {
type: "rule",
selector: prop,
declarations: value,
};
rules.push(rule);
}
}
return { rules, meta: {} };
};
export type StynWalk = (
tree: StynTree,
callback: (r: StynRule, parent: StynRule[], index: number) => void
) => StynTree;
export const walk: StynWalk = (tree, cb) => {
const treeCopy = { ...tree };
for (const rule of treeCopy.rules) {
cb(rule, treeCopy.rules, treeCopy.rules.indexOf(rule));
if (rule.type === "at-rule" && typeof rule.rules !== "undefined") {
for (const childRule of rule.rules) {
cb(childRule, rule.rules, rule.rules.indexOf(childRule));
}
}
}
return treeCopy;
};