Skip to content

Commit

Permalink
feat(meta-css): dedupe & merge all selectors & decls
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Dec 15, 2023
1 parent 34047c9 commit fd333c8
Showing 1 changed file with 42 additions and 40 deletions.
82 changes: 42 additions & 40 deletions packages/meta-css/src/convert.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IObjectOf } from "@thi.ng/api";
import { flag, string, strings, type Command } from "@thi.ng/args";
import { peek } from "@thi.ng/arrays";
import { illegalArgs, illegalState } from "@thi.ng/errors";
import { assert, illegalArgs, illegalState } from "@thi.ng/errors";
import { readJSON, readText } from "@thi.ng/file-io";
import {
COMPACT,
Expand All @@ -11,7 +11,8 @@ import {
type Format,
} from "@thi.ng/hiccup-css";
import { type ILogger } from "@thi.ng/logger";
import { basename, resolve } from "path";
import { map } from "@thi.ng/transducers";
import { resolve } from "path";
import type { AppCtx, CommonOpts, CompiledSpecs } from "./api.js";
import { maybeWriteText } from "./utils.js";

Expand All @@ -26,8 +27,6 @@ interface ConvertOpts extends CommonOpts {

interface Scope {
state: State;
decl: any[];
props: IObjectOf<any>;
sel: string[];
path: string;
parent?: Scope;
Expand All @@ -43,6 +42,7 @@ interface ProcessOpts {
logger: ILogger;
format: Format;
specs: CompiledSpecs;
plainRules: IObjectOf<Set<string>>;
mediaQueryIDs: Set<string>;
mediaQueryRules: IObjectOf<IObjectOf<Set<string>>>;
}
Expand Down Expand Up @@ -74,20 +74,20 @@ export const CONVERT: Command<ConvertOpts, CommonOpts, AppCtx<ConvertOpts>> = {
format: pretty ? PRETTY : COMPACT,
mediaQueryIDs: new Set(Object.keys(specs.media)),
mediaQueryRules: {},
plainRules: {},
};
const bundle: string[] = include
? include.map((x) => readText(resolve(x), logger).trim())
: [];
if (!noHeader) {
bundle.push(
`/*! generated by thi.ng/meta-css @ ${new Date().toISOString()} */`
);
}
for (let file of inputs) {
if (!noHeader) {
bundle.push(
`/*! generated by thi.ng/meta-css from ${basename(
file
)} @ ${new Date().toISOString()} */`
);
}
bundle.push(processSpec(readText(resolve(file), logger), procOpts));
processSpec(readText(resolve(file), logger), procOpts);
}
processPlainRules(bundle, procOpts);
processMediaQueries(bundle, procOpts);
maybeWriteText(out, bundle, logger);
},
Expand All @@ -98,23 +98,14 @@ const PATH_SEP = "///";

const defScope = (parent?: Scope): Scope => ({
state: "sel",
decl: [],
sel: parent ? [] : ["<root>"],
props: {},
path: "",
parent,
});

const endScope = (ctx: ProcessCtx) => {
const isEmpty = !ctx.curr.sel.length;
if (ctx.curr.parent) {
// don't add empty defs
if (!isEmpty && Object.keys(ctx.curr.props).length) {
ctx.curr.parent.decl.push(ctx.curr.decl);
}
} else {
illegalState("stack underflow");
}
assert(!!ctx.curr.parent, "stack underflow");
ctx.scopes.pop();
if (ctx.scopes.length > 0) {
ctx.curr = peek(ctx.scopes);
Expand All @@ -129,18 +120,23 @@ const endScope = (ctx: ProcessCtx) => {
const buildScopePath = (scopes: Scope[]) =>
scopes.map((x) => x.sel.join(",")).join(PATH_SEP);

const buildDecls = (
const buildDecls = (rules: IObjectOf<Set<string>>, specs: CompiledSpecs) =>
Object.entries(rules).map(([path, ids]) =>
buildDeclsForPath(path, ids, specs)
);

const buildDeclsForPath = (
selectorPath: string,
ids: string[],
ids: Iterable<string>,
specs: CompiledSpecs
) => {
const root: any[] = [];
let parent = root;
const levels = selectorPath.split(PATH_SEP);
for (let i = 0; i < levels.length; i++) {
const curr = levels[i].split(",");
if (i == levels.length - 1) {
curr.push(Object.assign({}, ...ids.map((x) => specs.defs[x])));
const parts = selectorPath.split(PATH_SEP);
for (let i = 0; i < parts.length; i++) {
const curr = parts[i].split(",");
if (i == parts.length - 1) {
curr.push(Object.assign({}, ...map((x) => specs.defs[x], ids)));
}
parent.push(curr);
parent = curr;
Expand Down Expand Up @@ -184,10 +180,8 @@ const processMediaQueries = (
{ logger, specs, format, mediaQueryRules }: ProcessOpts
) => {
for (let queryID in mediaQueryRules) {
const rules = Object.entries(mediaQueryRules[queryID]).map(
([path, ids]) => buildDecls(path, [...ids], specs)
);
logger.debug("responsive rules", queryID, rules);
const rules = buildDecls(mediaQueryRules[queryID], specs);
logger.debug("mediaquery rules", queryID, rules);
result.push(
css(at_media(mergeMediaQueries(specs.media, queryID), rules), {
format,
Expand All @@ -196,9 +190,18 @@ const processMediaQueries = (
}
};

const processPlainRules = (
result: string[],
{ logger, specs, format, plainRules }: ProcessOpts
) => {
const rules = buildDecls(plainRules, specs);
logger.debug("plain rules", rules);
result.push(css(rules, { format }));
};

const processSpec = (
src: string,
{ logger, format, specs, mediaQueryIDs, mediaQueryRules }: ProcessOpts
{ specs, mediaQueryIDs, mediaQueryRules, plainRules }: ProcessOpts
) => {
const root = defScope();
const initial = defScope(root);
Expand All @@ -216,7 +219,6 @@ const processSpec = (
if (token === "{") {
if ($scope.state === "sel") {
$scope.sel = $scope.sel.map((x) => x.replace(",", ""));
$scope.decl = [...$scope.sel, $scope.props];
$scope.path = buildScopePath(ctx.scopes);
}
$scope.state = "class";
Expand All @@ -242,8 +244,7 @@ const processSpec = (
token,
mediaQueryIDs
);
const spec = specs.defs[id];
if (!spec) illegalArgs(`unknown rule ID: ${id}`);
if (!specs.defs[id]) illegalArgs(`unknown rule ID: ${id}`);
if (query) {
if (!mediaQueryRules[query])
mediaQueryRules[query] = {};
Expand All @@ -252,14 +253,15 @@ const processSpec = (
(mediaQueryRules[query][$scope.path] = new Set())
).add(id);
} else {
Object.assign($scope.props, spec);
(
plainRules[$scope.path] ||
(plainRules[$scope.path] = new Set())
).add(id);
}
}
break;
default:
illegalState($scope.state);
}
}
logger.debug("root", root);
return css(root.decl, { format });
};

0 comments on commit fd333c8

Please sign in to comment.