mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
400 lines
9.8 KiB
TypeScript
400 lines
9.8 KiB
TypeScript
export const cssWideAndSpecialColorKeywords = new Set([
|
|
"transparent",
|
|
"currentcolor",
|
|
"inherit",
|
|
"initial",
|
|
"unset",
|
|
"revert",
|
|
]);
|
|
|
|
export const realNamedColors = [
|
|
"aliceblue",
|
|
"antiquewhite",
|
|
"aqua",
|
|
"aquamarine",
|
|
"azure",
|
|
"beige",
|
|
"bisque",
|
|
"black",
|
|
"blanchedalmond",
|
|
"blue",
|
|
"blueviolet",
|
|
"brown",
|
|
"burlywood",
|
|
"cadetblue",
|
|
"chartreuse",
|
|
"chocolate",
|
|
"coral",
|
|
"cornflowerblue",
|
|
"cornsilk",
|
|
"crimson",
|
|
"cyan",
|
|
"darkblue",
|
|
"darkcyan",
|
|
"darkgoldenrod",
|
|
"darkgray",
|
|
"darkgreen",
|
|
"darkgrey",
|
|
"darkkhaki",
|
|
"darkmagenta",
|
|
"darkolivegreen",
|
|
"darkorange",
|
|
"darkorchid",
|
|
"darkred",
|
|
"darksalmon",
|
|
"darkseagreen",
|
|
"darkslateblue",
|
|
"darkslategray",
|
|
"darkslategrey",
|
|
"darkturquoise",
|
|
"darkviolet",
|
|
"deeppink",
|
|
"deepskyblue",
|
|
"dimgray",
|
|
"dimgrey",
|
|
"dodgerblue",
|
|
"firebrick",
|
|
"floralwhite",
|
|
"forestgreen",
|
|
"fuchsia",
|
|
"gainsboro",
|
|
"ghostwhite",
|
|
"gold",
|
|
"goldenrod",
|
|
"gray",
|
|
"green",
|
|
"greenyellow",
|
|
"grey",
|
|
"honeydew",
|
|
"hotpink",
|
|
"indianred",
|
|
"indigo",
|
|
"ivory",
|
|
"khaki",
|
|
"lavender",
|
|
"lavenderblush",
|
|
"lawngreen",
|
|
"lemonchiffon",
|
|
"lightblue",
|
|
"lightcoral",
|
|
"lightcyan",
|
|
"lightgoldenrodyellow",
|
|
"lightgray",
|
|
"lightgreen",
|
|
"lightgrey",
|
|
"lightpink",
|
|
"lightsalmon",
|
|
"lightseagreen",
|
|
"lightskyblue",
|
|
"lightslategray",
|
|
"lightslategrey",
|
|
"lightsteelblue",
|
|
"lightyellow",
|
|
"lime",
|
|
"limegreen",
|
|
"linen",
|
|
"magenta",
|
|
"maroon",
|
|
"mediumaquamarine",
|
|
"mediumblue",
|
|
"mediumorchid",
|
|
"mediumpurple",
|
|
"mediumseagreen",
|
|
"mediumslateblue",
|
|
"mediumspringgreen",
|
|
"mediumturquoise",
|
|
"mediumvioletred",
|
|
"midnightblue",
|
|
"mintcream",
|
|
"mistyrose",
|
|
"moccasin",
|
|
"navajowhite",
|
|
"navy",
|
|
"oldlace",
|
|
"olive",
|
|
"olivedrab",
|
|
"orange",
|
|
"orangered",
|
|
"orchid",
|
|
"palegoldenrod",
|
|
"palegreen",
|
|
"paleturquoise",
|
|
"palevioletred",
|
|
"papayawhip",
|
|
"peachpuff",
|
|
"peru",
|
|
"pink",
|
|
"plum",
|
|
"powderblue",
|
|
"purple",
|
|
"rebeccapurple",
|
|
"red",
|
|
"rosybrown",
|
|
"royalblue",
|
|
"saddlebrown",
|
|
"salmon",
|
|
"sandybrown",
|
|
"seagreen",
|
|
"seashell",
|
|
"sienna",
|
|
"silver",
|
|
"skyblue",
|
|
"slateblue",
|
|
"slategray",
|
|
"slategrey",
|
|
"snow",
|
|
"springgreen",
|
|
"steelblue",
|
|
"tan",
|
|
"teal",
|
|
"thistle",
|
|
"tomato",
|
|
"turquoise",
|
|
"violet",
|
|
"wheat",
|
|
"white",
|
|
"whitesmoke",
|
|
"yellow",
|
|
"yellowgreen",
|
|
];
|
|
|
|
const cssDeclarationPattern = /(?:^|[;{])\s*[-_a-zA-Z][-_a-zA-Z0-9]*\s*:\s*(?<value>[^;{}]+)/g;
|
|
const cssNamedColors = new Set(realNamedColors);
|
|
|
|
export type CssNamedColorMatch = {
|
|
index: number;
|
|
value: string;
|
|
};
|
|
|
|
const cssHexColorPattern = /^#[0-9a-fA-F]{3,8}\b/;
|
|
|
|
export function collectCssNamedColorMatches(source: string): CssNamedColorMatch[] {
|
|
return collectCssHardcodedColorMatches(source).filter((match) => cssNamedColors.has(match.value.toLowerCase()));
|
|
}
|
|
|
|
export function collectCssHardcodedColorMatches(source: string): CssNamedColorMatch[] {
|
|
const matches: CssNamedColorMatch[] = [];
|
|
const scannableSource = maskCssCommentsAndStrings(source);
|
|
|
|
for (const declaration of scannableSource.matchAll(cssDeclarationPattern)) {
|
|
const declarationValue = declaration.groups?.value;
|
|
if (declarationValue === undefined) continue;
|
|
|
|
const valueOffset = (declaration.index ?? 0) + declaration[0].lastIndexOf(declarationValue);
|
|
matches.push(...collectCssHardcodedColorMatchesFromDeclarationValue(declarationValue, valueOffset));
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
function maskCssCommentsAndStrings(source: string): string {
|
|
const characters = source.split("");
|
|
let index = 0;
|
|
|
|
while (index < characters.length) {
|
|
const current = characters[index];
|
|
const next = characters[index + 1];
|
|
|
|
if (current === "/" && next === "*") {
|
|
const endIndex = source.indexOf("*/", index + 2);
|
|
const exclusiveEnd = endIndex === -1 ? characters.length : endIndex + 2;
|
|
maskRange(characters, index, exclusiveEnd);
|
|
index = exclusiveEnd;
|
|
continue;
|
|
}
|
|
|
|
if (current === '"' || current === "'") {
|
|
const exclusiveEnd = skipCssString(source, index, current);
|
|
maskRange(characters, index, exclusiveEnd);
|
|
index = exclusiveEnd;
|
|
continue;
|
|
}
|
|
|
|
index += 1;
|
|
}
|
|
|
|
return characters.join("");
|
|
}
|
|
|
|
function maskRange(characters: string[], startIndex: number, exclusiveEnd: number): void {
|
|
for (let index = startIndex; index < exclusiveEnd; index += 1) {
|
|
if (characters[index] !== "\n") characters[index] = " ";
|
|
}
|
|
}
|
|
|
|
function collectCssHardcodedColorMatchesFromDeclarationValue(
|
|
declarationValue: string,
|
|
sourceOffset: number,
|
|
): CssNamedColorMatch[] {
|
|
const matches: CssNamedColorMatch[] = [];
|
|
let index = 0;
|
|
|
|
while (index < declarationValue.length) {
|
|
const current = declarationValue[index];
|
|
const next = declarationValue[index + 1];
|
|
|
|
if (current === "/" && next === "*") {
|
|
const commentEnd = declarationValue.indexOf("*/", index + 2);
|
|
index = commentEnd === -1 ? declarationValue.length : commentEnd + 2;
|
|
continue;
|
|
}
|
|
|
|
if (current === '"' || current === "'") {
|
|
index = skipCssString(declarationValue, index, current);
|
|
continue;
|
|
}
|
|
|
|
const hexColor = declarationValue.slice(index).match(cssHexColorPattern)?.[0];
|
|
if (hexColor !== undefined) {
|
|
matches.push({ index: sourceOffset + index, value: hexColor });
|
|
index += hexColor.length;
|
|
continue;
|
|
}
|
|
|
|
const functionName = readCssIdentifier(declarationValue, index);
|
|
if (functionName !== undefined && functionName.value.toLowerCase() === "url") {
|
|
const functionStart = skipCssWhitespace(declarationValue, functionName.endIndex);
|
|
if (declarationValue[functionStart] === "(") {
|
|
index = skipCssFunction(declarationValue, functionStart);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (functionName !== undefined && functionName.value.toLowerCase() === "var") {
|
|
const functionStart = skipCssWhitespace(declarationValue, functionName.endIndex);
|
|
if (declarationValue[functionStart] === "(") {
|
|
const functionEnd = skipCssFunction(declarationValue, functionStart);
|
|
const fallbackStart = cssVarFallbackStartIndex(declarationValue, functionStart, functionEnd);
|
|
if (fallbackStart !== undefined) {
|
|
matches.push(
|
|
...collectCssHardcodedColorMatchesFromDeclarationValue(
|
|
declarationValue.slice(fallbackStart, functionEnd - 1),
|
|
sourceOffset + fallbackStart,
|
|
),
|
|
);
|
|
}
|
|
index = functionEnd;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (functionName !== undefined && ["rgb", "rgba", "hsl", "hsla"].includes(functionName.value.toLowerCase())) {
|
|
const functionStart = skipCssWhitespace(declarationValue, functionName.endIndex);
|
|
if (declarationValue[functionStart] === "(") {
|
|
const functionEnd = skipCssFunction(declarationValue, functionStart);
|
|
matches.push({ index: sourceOffset + index, value: declarationValue.slice(index, functionEnd) });
|
|
index = functionEnd;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const identifier = readCssIdentifier(declarationValue, index);
|
|
if (identifier === undefined) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
const normalizedValue = identifier.value.toLowerCase();
|
|
if (cssNamedColors.has(normalizedValue) && !cssWideAndSpecialColorKeywords.has(normalizedValue)) {
|
|
matches.push({ index: sourceOffset + index, value: identifier.value });
|
|
}
|
|
|
|
index = identifier.endIndex;
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
function readCssIdentifier(source: string, startIndex: number): { value: string; endIndex: number } | undefined {
|
|
const start = source[startIndex];
|
|
if (start === undefined || !/[A-Za-z_]/.test(start)) return undefined;
|
|
|
|
let endIndex = startIndex + 1;
|
|
while (endIndex < source.length && /[-_A-Za-z0-9]/.test(source[endIndex] ?? "")) {
|
|
endIndex += 1;
|
|
}
|
|
|
|
return { value: source.slice(startIndex, endIndex), endIndex };
|
|
}
|
|
|
|
function skipCssString(source: string, startIndex: number, quote: string): number {
|
|
let index = startIndex + 1;
|
|
while (index < source.length) {
|
|
const current = source[index];
|
|
if (current === "\\") {
|
|
index += 2;
|
|
continue;
|
|
}
|
|
|
|
if (current === quote) return index + 1;
|
|
index += 1;
|
|
}
|
|
|
|
return source.length;
|
|
}
|
|
|
|
function skipCssWhitespace(source: string, startIndex: number): number {
|
|
let index = startIndex;
|
|
while (index < source.length && /\s/.test(source[index] ?? "")) index += 1;
|
|
return index;
|
|
}
|
|
|
|
function skipCssFunction(source: string, openParenIndex: number): number {
|
|
let depth = 1;
|
|
let index = openParenIndex + 1;
|
|
|
|
while (index < source.length) {
|
|
const current = source[index];
|
|
const next = source[index + 1];
|
|
|
|
if (current === "/" && next === "*") {
|
|
const commentEnd = source.indexOf("*/", index + 2);
|
|
index = commentEnd === -1 ? source.length : commentEnd + 2;
|
|
continue;
|
|
}
|
|
|
|
if (current === '"' || current === "'") {
|
|
index = skipCssString(source, index, current);
|
|
continue;
|
|
}
|
|
|
|
if (current === "(") depth += 1;
|
|
if (current === ")") {
|
|
depth -= 1;
|
|
if (depth === 0) return index + 1;
|
|
}
|
|
|
|
index += 1;
|
|
}
|
|
|
|
return source.length;
|
|
}
|
|
|
|
function cssVarFallbackStartIndex(source: string, openParenIndex: number, functionEndIndex: number): number | undefined {
|
|
let depth = 0;
|
|
let index = openParenIndex + 1;
|
|
|
|
while (index < functionEndIndex - 1) {
|
|
const current = source[index];
|
|
const next = source[index + 1];
|
|
|
|
if (current === "/" && next === "*") {
|
|
const commentEnd = source.indexOf("*/", index + 2);
|
|
index = commentEnd === -1 ? functionEndIndex - 1 : Math.min(commentEnd + 2, functionEndIndex - 1);
|
|
continue;
|
|
}
|
|
|
|
if (current === '"' || current === "'") {
|
|
index = skipCssString(source, index, current);
|
|
continue;
|
|
}
|
|
|
|
if (current === "(") depth += 1;
|
|
if (current === ")") depth -= 1;
|
|
if (current === "," && depth === 0) return index + 1;
|
|
|
|
index += 1;
|
|
}
|
|
|
|
return undefined;
|
|
}
|