core/parsers/css.parser.ts
core/parsers/css.parser.ts is a file in GovLab Quality. 154 lines of code and 39 definitions.
import { BASE_COMPONENTS, COMPONENT_BOUNDARIES } from "#configuration/constants/validation.constants";
import { CLAMP_OPEN, CLAMP_PART_COUNT } from "#configuration/constants/css.constants";
import type { ClampParts, UnitToken } from "#types/css.types";
import { isAsciiAlpha, isCssNameChar } from "#core/predicates/code-point.predicate";
import valueParser from "postcss-value-parser";
const VAR_PREFIX = "--";
const VAR_CALL = "var(";
const VAR_USE = "var(--";
const nameEnd = function nameEnd(content: string, from: number): number {
let end = from;
while (end < content.length && isCssNameChar(content.codePointAt(end))) {
end += 1;
}
return end;
};
const skipInlineSpace = function skipInlineSpace(content: string, from: number): number {
let after = from;
while (after < content.length && (content[after] === " " || content[after] === "\t")) {
after += 1;
}
return after;
};
const isVarDefinition = function isVarDefinition(content: string, idx: number, end: number): boolean {
const isUse = idx >= VAR_CALL.length && content.slice(idx - VAR_CALL.length, idx) === VAR_CALL;
return !isUse && end > idx + VAR_PREFIX.length && content[skipInlineSpace(content, end)] === ":";
};
const varSpans = function varSpans(content: string): { end: number; idx: number }[] {
const spans: { end: number; idx: number }[] = [];
let idx = content.indexOf(VAR_PREFIX, 0);
while (idx !== -1) {
const end = nameEnd(content, idx + VAR_PREFIX.length);
spans.push({ end, idx });
idx = content.indexOf(VAR_PREFIX, end);
}
return spans;
};
export const definedVars = function definedVars(content: string): string[] {
return varSpans(content)
.filter((span) => isVarDefinition(content, span.idx, span.end))
.map((span) => content.slice(span.idx, span.end));
};
export const scriptVarRefs = function scriptVarRefs(content: string): string[] {
return varSpans(content)
.filter((span) => span.end > span.idx + VAR_PREFIX.length)
.map((span) => content.slice(span.idx, span.end));
};
export const usedVars = function usedVars(content: string): string[] {
const names: string[] = [];
let idx = content.indexOf(VAR_USE, 0);
while (idx !== -1) {
const start = idx + VAR_CALL.length;
const end = nameEnd(content, start);
names.push(content.slice(start, end));
idx = content.indexOf(VAR_USE, end);
}
return names;
};
const selectorDefinesComponent = function selectorDefinesComponent(selector: string, pattern: string): boolean {
if (!selector.startsWith(`.${pattern}`)) {
return false;
}
const after = selector.slice(1 + pattern.length);
return after.length === 0 || COMPONENT_BOUNDARIES.has(after.charAt(0));
};
export const componentsInFile = function componentsInFile(content: string): Set<string> {
const selectors = content.split("\n").flatMap((raw) => {
const braceIdx = raw.indexOf("{");
return braceIdx === -1
? []
: raw
.slice(0, braceIdx)
.split(",")
.map((selector) => selector.trim());
});
return new Set(
BASE_COMPONENTS.filter((pattern) => selectors.some((selector) => selectorDefinesComponent(selector, pattern))),
);
};
export const valueUnits = function valueUnits(value: string): UnitToken[] {
const out: UnitToken[] = [];
valueParser(value).walk((node) => {
if (node.type === "word") {
const parsed = valueParser.unit(node.value);
if (parsed !== false && parsed.unit !== "") {
out.push({ number: parsed.number, unit: parsed.unit });
}
}
});
return out;
};
const scanClassName = function scanClassName(selector: string, start: number): number {
let i = start;
while (i < selector.length && isCssNameChar(selector.codePointAt(i))) {
i += 1;
}
return i;
};
export const extractClassNames = function extractClassNames(selector: string): string[] {
const names: string[] = [];
let i = 0;
while (i < selector.length) {
if (selector[i] === "." && i + 1 < selector.length && isAsciiAlpha(selector.codePointAt(i + 1))) {
const end = scanClassName(selector, i + 1);
names.push(selector.slice(i + 1, end));
i = end;
} else {
i += 1;
}
}
return names;
};
const PAREN_DELTA = new Map([
["(", 1],
[")", -1],
]);
const matchParen = function matchParen(value: string, from: number): number {
let depth = 1;
let pos = from;
while (pos < value.length && depth > 0) {
depth += PAREN_DELTA.get(value[pos] ?? "") ?? 0;
pos += 1;
}
return depth === 0 ? pos : -1;
};
const parseClampAt = function parseClampAt(value: string, idx: number): { clamp: ClampParts | null; next: number } {
const contentStart = idx + CLAMP_OPEN.length;
const end = matchParen(value, contentStart);
if (end === -1) {
return { clamp: null, next: value.length };
}
const parts = value.slice(contentStart, end - 1).split(",");
if (parts.length !== CLAMP_PART_COUNT) {
return { clamp: null, next: end };
}
const [min = "", preferred = "", max = ""] = parts;
return {
clamp: { full: value.slice(idx, end), max: max.trim(), min: min.trim(), preferred: preferred.trim() },
next: end,
};
};
export const extractClamps = function extractClamps(value: string): ClampParts[] {
const results: ClampParts[] = [];
let searchFrom = 0;
while (searchFrom < value.length) {
const idx = value.indexOf(CLAMP_OPEN, searchFrom);
if (idx === -1) {
break;
}
const { clamp, next } = parseClampAt(value, idx);
results.push(...(clamp ? [clamp] : []));
searchFrom = next;
}
return results;
};