core/factories/axis.factory.ts
core/factories/axis.factory.ts is a file in GovLab Quality. 186 lines of code and 45 definitions.
import type {
AxisContext,
AxisMessages,
AxisPlugins,
AxisRuleInputs,
AxisSecondaryOptions,
AxisSpec,
ForeignFn,
RedefinedFn,
} from "#types/axis.types";
import type { Rule as CssRule, Root } from "postcss";
import stylelint, { type Plugin, type PostcssResult, type Rule } from "stylelint";
import type { GovlabStylelintMeta } from "#types/stylelint.types";
import type { TypeSystemLayout } from "#types/config.types";
import { withRuleId } from "#core/formatters/stylelint.formatter";
const { createPlugin, utils } = stylelint;
const COMBINATORS = new Set([" ", ">", "+", "~", "\t", "\n"]);
const BRACKET_DELTA = new Map([
["[", 1],
["]", -1],
]);
const subjectCompound = function subjectCompound(selector: string): string {
let depth = 0;
let lastCombinator = -1;
for (let i = 0; i < selector.length; i += 1) {
const c = selector[i] ?? "";
const delta = BRACKET_DELTA.get(c);
if (delta !== undefined) {
depth += delta;
continue;
}
if (depth === 0 && COMBINATORS.has(c)) {
lastCombinator = i;
}
}
return selector.slice(lastCombinator + 1);
};
const attrValueEnd = function attrValueEnd(selector: string, from: number): number {
let i = from;
while (i < selector.length && selector[i] !== '"' && selector[i] !== "'" && selector[i] !== "]") {
i += 1;
}
return i;
};
const stepValue = function stepValue(selector: string, attrToken: string): string | null {
const start = selector.indexOf(attrToken);
const eq = start === -1 ? -1 : selector.indexOf("=", start);
if (eq === -1) {
return null;
}
const afterEq = eq + 1;
const quoted = selector[afterEq] === '"' || selector[afterEq] === "'";
const valueStart = quoted ? afterEq + 1 : afterEq;
return selector.slice(valueStart, attrValueEnd(selector, valueStart));
};
const buildMessages = function buildMessages(spec: AxisSpec): AxisMessages {
const { attrToken, label, owns, ruleId, ruleName } = spec;
const foreign: ForeignFn = (selector, home) =>
withRuleId(
`Selector '${selector}' defines a step of the ${label} scale outside ${home}, which owns it. Every step of ${owns} is declared there once and referenced elsewhere; a rule matching ${attrToken}] anywhere else redefines the scale for one surface. Remove this rule and reference the step; a step that does not exist yet is added to the owning file and its declared vocabulary.`,
ruleId,
);
const redefined: RedefinedFn = (step) =>
withRuleId(
`${label} step '${attrToken}="${step}"]' is defined more than once in the file that owns it. A step is one rule — merge the declarations into it.`,
ruleId,
);
return { foreign, messages: utils.ruleMessages(ruleName, { foreign, redefined }), redefined };
};
const handleStep = function handleStep(cssRule: CssRule, ctx: AxisContext, seen: Set<string>): void {
const step = stepValue(cssRule.selector, ctx.attrToken);
if (step === null || !ctx.validSteps.has(step)) {
return;
}
if (seen.has(step)) {
utils.report({
message: ctx.redefined(step),
node: cssRule,
result: ctx.result,
ruleName: ctx.ruleName,
word: cssRule.selector,
});
}
seen.add(step);
};
const checkCssRule = function checkCssRule(cssRule: CssRule, ctx: AxisContext, seen: Set<string>): void {
if (!cssRule.selector.includes(ctx.attrToken)) {
return;
}
if (!ctx.isHome) {
utils.report({
message: ctx.foreign(cssRule.selector, ctx.home),
node: cssRule,
result: ctx.result,
ruleName: ctx.ruleName,
word: cssRule.selector,
});
return;
}
if (subjectCompound(cssRule.selector).includes(ctx.attrToken)) {
handleStep(cssRule, ctx, seen);
}
};
const isSecondaryOptions = function isSecondaryOptions(value: unknown): value is AxisSecondaryOptions {
return typeof value === "object" && value !== null;
};
const axisOptions = function axisOptions(
primary: unknown,
secondaryOptions: unknown,
): { home: string; steps: string[] } | null {
if (primary !== true) {
return null;
}
const opts: AxisSecondaryOptions = isSecondaryOptions(secondaryOptions) ? secondaryOptions : {};
const home = typeof opts.home === "string" ? opts.home : "";
const steps = Array.isArray(opts.steps) ? opts.steps : [];
return home.length === 0 || steps.length === 0 ? null : { home, steps };
};
const applyAxisRule = function applyAxisRule(inputs: AxisRuleInputs, root: Root, result: PostcssResult): void {
const options = axisOptions(inputs.primary, inputs.secondaryOptions);
if (options === null) {
return;
}
const { home, steps } = options;
const from = (root.source?.input.from ?? "").split("\\").join("/");
const ctx: AxisContext = {
attrToken: inputs.spec.attrToken,
foreign: inputs.built.foreign,
home,
isHome: from.endsWith(home),
redefined: inputs.built.redefined,
result,
ruleName: inputs.spec.ruleName,
validSteps: new Set(steps),
};
const seen = new Set<string>();
root.walkRules((cssRule: CssRule): void => {
checkCssRule(cssRule, ctx, seen);
});
};
const AXIS_CANON = ["design-tokens", "css-architecture"];
const axisSpecFor = function axisSpecFor(axis: string): AxisSpec {
return {
attrToken: `[data-${axis}`,
label: axis,
meta: {
canonical: AXIS_CANON,
description: `Every step of the ${axis} axis is defined once, in the file that owns the axis`,
fixable: false,
},
owns: `the ${axis} axis`,
ruleId: `${axis}_axis_definition`,
ruleName: `govlab/${axis}-axis`,
};
};
export const axisPluginsFor = function axisPluginsFor(typeSystem?: TypeSystemLayout): AxisPlugins {
const homes: Record<string, string | undefined> = { ...typeSystem?.axisHomes };
const out: AxisPlugins = { meta: [], plugins: [], rules: {} };
for (const [axis, steps] of Object.entries(typeSystem?.axes ?? {})) {
const home = homes[axis];
if (typeof home === "string" && Array.isArray(steps) && steps.length > 0) {
const spec = axisSpecFor(axis);
const built = axisDefinitionRule(spec);
out.meta.push(built.RULE_META);
out.plugins.push(built.rule);
out.rules[spec.ruleName] = [true, { home, steps }];
}
}
return out;
};
export const axisDefinitionRule = function axisDefinitionRule(spec: AxisSpec): {
RULE_META: GovlabStylelintMeta;
rule: Plugin;
} {
const { ruleName } = spec;
const RULE_META: GovlabStylelintMeta = { meta: spec.meta, ruleId: spec.ruleId, ruleName };
const built = buildMessages(spec);
const rule: Rule = Object.assign(
(primary: unknown, secondaryOptions: unknown) =>
(root: Root, result: PostcssResult): void => {
applyAxisRule({ built, primary, secondaryOptions, spec }, root, result);
},
{ messages: built.messages, ruleName },
);
return { RULE_META, rule: createPlugin(ruleName, rule) };
};