core/stylelint/shrinkable-grid-track.stylelint.rule.ts

core/stylelint/shrinkable-grid-track.stylelint.rule.ts is a file in GovLab Quality. 57 lines of code and 11 definitions.

import type { Declaration, Root } from "postcss";
import stylelint, { type PostcssResult, type Rule } from "stylelint";
import type { GovlabStylelintMeta } from "#types/stylelint.types";
import valueParser, { type Node } from "postcss-value-parser";
import { withRuleId } from "#core/formatters/stylelint.formatter";

const { createPlugin, utils } = stylelint;
const ruleName = "govlab/shrinkable-grid-track";
const ruleId = "shrinkable_grid_track";

export const RULE_META: GovlabStylelintMeta = {
    meta: {
        canonical: ["responsive-design"],
        description: "Require flexible grid column tracks to be able to shrink below their content",
        fixable: false,
    },
    ruleId,
    ruleName,
};

const TRACK_PROPS = new Set(["grid-template-columns", "grid-auto-columns"]);
const FLEX_UNIT = "fr";
const SHRINKING_FUNCTION = "minmax";

const messages = utils.ruleMessages(ruleName, {
    bareFraction: (track: string): string =>
        withRuleId(
            `The column track '${track}' is a bare fraction, which never shrinks below its content, so one long word or code line pushes the grid wider than a narrow screen. Write the track as minmax(0, ${track}) so it can shrink to the space it is given.`,
            ruleId,
        ),
});

const bareFractions = function bareFractions(nodes: readonly Node[], shrinkable: boolean): string[] {
    return nodes.flatMap((node) => {
        if (node.type === "function") {
            return bareFractions(node.nodes, shrinkable || node.value.toLowerCase() === SHRINKING_FUNCTION);
        }
        if (node.type !== "word" || shrinkable) {
            return [];
        }
        const parsed = valueParser.unit(node.value);
        return parsed !== false && parsed.unit.toLowerCase() === FLEX_UNIT ? [node.value] : [];
    });
};

const rule: Rule = Object.assign(
    (primary: unknown) =>
        (root: Root, result: PostcssResult): void => {
            if (primary !== true) {
                return;
            }
            root.walkDecls((decl: Declaration) => {
                if (!TRACK_PROPS.has(decl.prop.toLowerCase())) {
                    return;
                }
                for (const track of new Set(bareFractions(valueParser(decl.value).nodes, false))) {
                    utils.report({ message: messages.bareFraction(track), node: decl, result, ruleName, word: track });
                }
            });
        },
    { messages, ruleName },
);

export default createPlugin(ruleName, rule);