w1-system-font-validator
Version:
VS Code extension for validating W1 System font variables (both fontConfig.json and localFontConfig.json)
627 lines • 28.2 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.UnifiedFontValidator = void 0;
const vscode = __importStar(require("vscode"));
const configDetector_1 = require("../config/configDetector");
class UnifiedFontValidator {
constructor() {
this.validationTimeouts = new Map();
this.projectConfigs = new Map();
// NEW SYSTEM: Only font-family variables, no weight-specific variables
this.FONT_FAMILY_PATTERN = /var\(\s*(-{2}fontfamily_[a-zA-Z0-9_]+)\s*\)/g;
// Pattern for custom font variables (like --typo_*, --font_*)
this.CUSTOM_FONT_PATTERN = /var\(\s*(-{2}(?:typo|font)_[a-zA-Z0-9_]+)\s*\)/g;
// NEW: Pattern for ANY var() usage in font-family properties
this.FONT_FAMILY_PROPERTY_PATTERN = /font-family\s*:\s*[^;]*var\(\s*(-{2}[a-zA-Z0-9_]+)\s*\)[^;]*;?/gi;
// Pattern to find CSS variable definitions
this.CSS_VAR_DEFINITION_PATTERN = /(-{2}[a-zA-Z0-9_]+)\s*:\s*([^;]+);/g;
// Valid CSS font-weight values (numeric and keyword)
this.VALID_FONT_WEIGHTS = ["100", "200", "300", "400", "500", "600", "700", "800", "900", "normal", "bold", "lighter", "bolder"];
// CSS property patterns for context validation
this.FONT_WEIGHT_PATTERN = /font-weight:\s*([^;]+);?/gi;
this.FONT_STYLE_PATTERN = /font-style:\s*(normal|italic|oblique);?/gi;
this.diagnosticCollection = vscode.languages.createDiagnosticCollection("w1FontValidator");
this.configDetector = new configDetector_1.ConfigDetector();
}
/**
* Validate a document with debouncing
*/
validateDocumentDebounced(document) {
const existingTimeout = this.validationTimeouts.get(document.uri.toString());
if (existingTimeout) {
clearTimeout(existingTimeout);
}
const timeout = setTimeout(() => {
this.validateDocument(document);
this.validationTimeouts.delete(document.uri.toString());
}, 300);
this.validationTimeouts.set(document.uri.toString(), timeout);
}
/**
* Main validation method
*/
async validateDocument(document) {
try {
if (!this.shouldValidateDocument(document)) {
return;
}
// Get or load project configuration
const projectConfig = await this.getProjectConfig(document.fileName);
if (!projectConfig.hasFontConfig && !projectConfig.hasLocalFontConfig) {
// No font configurations found - clear any existing diagnostics
this.diagnosticCollection.set(document.uri, []);
return;
}
const text = document.getText();
const diagnostics = [];
// Validate NEW SYSTEM: font-family variables
this.validateFontFamilyVariables(document, text, projectConfig, diagnostics);
// NEW: Validate ANY var() usage in font-family properties
this.validateFontFamilyProperties(document, text, projectConfig, diagnostics);
// Validate custom font variables (--typo_*, --font_*)
try {
await this.validateCustomFontVariables(document, text, projectConfig, diagnostics);
}
catch (error) {
console.error(`[W1 Font Validator] Custom validation error:`, error);
}
// Validate font-weight and font-style combinations
this.validateFontWeightAndStyle(document, text, projectConfig, diagnostics);
this.diagnosticCollection.set(document.uri, diagnostics);
}
catch (error) {
console.error(`[W1 Font Validator] Error validating document:`, error);
}
}
/**
* NEW SYSTEM: Validate font-family variables (--fontfamily_*)
*/
validateFontFamilyVariables(document, text, projectConfig, diagnostics) {
const validFontFamilies = this.configDetector.getValidFontFamilies(projectConfig);
let match;
// Reset regex state
this.FONT_FAMILY_PATTERN.lastIndex = 0;
while ((match = this.FONT_FAMILY_PATTERN.exec(text)) !== null) {
const fullMatch = match[0];
const variableName = match[1];
const startPos = document.positionAt(match.index);
const endPos = document.positionAt(match.index + fullMatch.length);
if (!validFontFamilies.has(variableName)) {
const suggestions = this.getSuggestions(variableName, validFontFamilies);
const diagnostic = new vscode.Diagnostic(new vscode.Range(startPos, endPos), `Invalid font-family variable: ${variableName}. ${suggestions.length > 0 ? `Did you mean: ${suggestions.join(", ")}?` : "No valid font families found."}`, vscode.DiagnosticSeverity.Error);
diagnostic.tags = [vscode.DiagnosticTag.Unnecessary];
diagnostics.push(diagnostic);
}
}
}
/**
* NEW: Validate ANY var() usage in font-family properties (catches non-W1 variables)
*/
validateFontFamilyProperties(document, text, projectConfig, diagnostics) {
const validFontFamilies = this.configDetector.getValidFontFamilies(projectConfig);
let match;
// Reset regex state
this.FONT_FAMILY_PROPERTY_PATTERN.lastIndex = 0;
while ((match = this.FONT_FAMILY_PROPERTY_PATTERN.exec(text)) !== null) {
const fullMatch = match[0];
const variableName = match[1];
// Skip if this is already a valid W1 font variable (handled by validateFontFamilyVariables)
if (validFontFamilies.has(variableName)) {
continue;
}
// Skip if this is a typo/font custom variable (handled by validateCustomFontVariables)
if (variableName.startsWith("--typo_") || variableName.startsWith("--font_")) {
continue;
}
// Find the position of the variable within the full match
const varIndex = fullMatch.indexOf(`var(${variableName})`);
const startPos = document.positionAt(match.index + varIndex + 4); // +4 for "var("
const endPos = document.positionAt(match.index + varIndex + 4 + variableName.length);
// This is an invalid variable in font-family property
const suggestions = this.getSuggestions(variableName, validFontFamilies);
const diagnostic = new vscode.Diagnostic(new vscode.Range(startPos, endPos), `Invalid variable in font-family: ${variableName}. ${suggestions.length > 0 ? `Did you mean: ${suggestions.join(", ")}?` : "Use a valid W1 font variable (--fontfamily_*)."}`, vscode.DiagnosticSeverity.Error);
diagnostic.tags = [vscode.DiagnosticTag.Unnecessary];
diagnostic.source = "W1 Font Validator - Font Family Properties";
diagnostics.push(diagnostic);
}
}
/**
* NEW: Validate custom font variables by checking their definitions in global CSS files
*/
async validateCustomFontVariables(document, text, projectConfig, diagnostics) {
const validFontFamilies = this.configDetector.getValidFontFamilies(projectConfig);
let match;
// Reset regex state
this.CUSTOM_FONT_PATTERN.lastIndex = 0;
while ((match = this.CUSTOM_FONT_PATTERN.exec(text)) !== null) {
const fullMatch = match[0];
const variableName = match[1];
const startPos = document.positionAt(match.index);
const endPos = document.positionAt(match.index + fullMatch.length);
// Check if this custom variable is defined using valid fontConfig variables
const isValidDefinition = await this.isCustomVariableValidlyDefined(variableName, projectConfig, validFontFamilies);
if (!isValidDefinition.isValid) {
let message;
if (isValidDefinition.foundDefinition) {
message = `Custom font variable ${variableName} is defined using invalid font: ${isValidDefinition.definedWith}. Use a valid fontConfig variable instead.`;
}
else {
message = `Custom font variable ${variableName} definition not found in global CSS files. Ensure it's defined using a valid fontConfig variable.`;
}
const diagnostic = new vscode.Diagnostic(new vscode.Range(startPos, endPos), message, vscode.DiagnosticSeverity.Error);
diagnostic.source = "W1 Font Validator - Custom Variables";
diagnostics.push(diagnostic);
}
}
}
/**
* Check if a custom variable is defined using valid fontConfig variables
*/
async isCustomVariableValidlyDefined(variableName, projectConfig, validFontFamilies) {
try {
// Find global CSS files in the project
const globalCssFiles = await this.findGlobalCssFiles();
for (const cssFile of globalCssFiles) {
const cssContent = await this.readFileContent(cssFile);
if (!cssContent)
continue;
// Look for the variable definition
const definitionMatch = this.findVariableDefinition(cssContent, variableName);
if (definitionMatch) {
// Check if the definition uses a valid fontConfig variable
const usesValidFont = this.definitionUsesValidFont(definitionMatch.value, validFontFamilies);
return {
isValid: usesValidFont,
foundDefinition: true,
definedWith: definitionMatch.value,
};
}
}
return { isValid: false, foundDefinition: false };
}
catch (error) {
console.error(`[W1 Font Validator] Error checking custom variable ${variableName}:`, error);
return { isValid: true, foundDefinition: false }; // Assume valid on error to avoid false positives
}
}
/**
* Find global CSS files in the workspace
*/
async findGlobalCssFiles() {
const globalFiles = [];
// Search for global CSS files
const patterns = ["**/global.css", "**/globals.css", "**/global.scss", "**/globals.scss"];
for (const pattern of patterns) {
const files = await vscode.workspace.findFiles(pattern, "**/node_modules/**");
globalFiles.push(...files);
}
return globalFiles;
}
/**
* Read file content safely
*/
async readFileContent(uri) {
try {
const document = await vscode.workspace.openTextDocument(uri);
return document.getText();
}
catch (error) {
console.error(`[W1 Font Validator] Error reading file ${uri.fsPath}:`, error);
return null;
}
}
/**
* Find a variable definition in CSS content
*/
findVariableDefinition(cssContent, variableName) {
// Escape special regex characters in variable name
const escapedVarName = variableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const regex = new RegExp(`${escapedVarName}\\s*:\\s*([^;]+);`, "gi");
const match = regex.exec(cssContent);
if (match) {
return { value: match[1].trim() };
}
return null;
}
/**
* Check if a CSS value uses valid fontConfig variables
*/
definitionUsesValidFont(value, validFontFamilies) {
// Check if the value contains a valid fontConfig variable
const fontFamilyMatches = value.match(/var\(\s*(-{2}fontfamily_[a-zA-Z0-9_]+)\s*\)/g);
if (fontFamilyMatches) {
// Extract variable names and check if they're valid
for (const match of fontFamilyMatches) {
const varMatch = match.match(/var\(\s*(-{2}fontfamily_[a-zA-Z0-9_]+)\s*\)/);
if (varMatch && validFontFamilies.has(varMatch[1])) {
return true;
}
}
return false; // Found fontfamily variables but none were valid
}
// If no fontfamily variables found, assume it might be a fallback font (valid)
return true;
}
/**
* ENHANCED: Validate font-weight and font-style combinations against available font data
*/
validateFontWeightAndStyle(document, text, projectConfig, diagnostics) {
const lines = text.split("\n");
lines.forEach((line, lineIndex) => {
// Check for font-weight declarations
const fontWeightMatch = line.match(/font-weight:\s*([^;]+);?/i);
if (!fontWeightMatch)
return;
const weightValue = fontWeightMatch[1].trim();
const weightStartIndex = line.indexOf(fontWeightMatch[1]);
// Find the font-family variable used in this CSS rule
const fontFamilyVar = this.findFontFamilyInRule(lines, lineIndex);
if (fontFamilyVar) {
// Validate if the weight is available for this specific font
const isValidCombination = this.isValidWeightStyleCombination(projectConfig, fontFamilyVar, weightValue, this.findFontStyleInRule(lines, lineIndex));
if (!isValidCombination.isValid) {
const startPos = new vscode.Position(lineIndex, weightStartIndex);
const endPos = new vscode.Position(lineIndex, weightStartIndex + weightValue.length);
const availableWeights = this.getAvailableWeightsForFont(projectConfig, fontFamilyVar);
const message = `Font weight ${weightValue} not available for ${fontFamilyVar}. Available weights: ${availableWeights.join(", ")}`;
const diagnostic = new vscode.Diagnostic(new vscode.Range(startPos, endPos), message, vscode.DiagnosticSeverity.Error);
diagnostic.source = "W1 Font Validator";
diagnostics.push(diagnostic);
}
}
else if (!this.isValidFontWeight(weightValue)) {
// General validation for non-W1 fonts
const startPos = new vscode.Position(lineIndex, weightStartIndex);
const endPos = new vscode.Position(lineIndex, weightStartIndex + weightValue.length);
const diagnostic = new vscode.Diagnostic(new vscode.Range(startPos, endPos), `Invalid font-weight value: ${weightValue}. Use standard CSS weights: 100-900, normal, bold, etc.`, vscode.DiagnosticSeverity.Error);
diagnostics.push(diagnostic);
}
});
}
/**
* Find font-family variable in the current CSS rule context
*/
findFontFamilyInRule(lines, lineIndex) {
// Look for font-family in the same CSS rule (within braces)
let braceLevel = 0;
let foundOpenBrace = false;
// Search backwards to find the start of the rule
for (let i = lineIndex; i >= 0; i--) {
const line = lines[i];
// Count braces to understand rule boundaries
for (const char of line) {
if (char === "{") {
braceLevel++;
foundOpenBrace = true;
}
else if (char === "}") {
braceLevel--;
}
}
// Check for font-family variable in this line
const fontFamilyMatch = line.match(/font-family:\s*var\(\s*(-{2}fontfamily_[a-zA-Z0-9_]+)\s*\)/);
if (fontFamilyMatch) {
return fontFamilyMatch[1];
}
// Stop if we've gone outside the current rule
if (foundOpenBrace && braceLevel <= 0) {
break;
}
}
// Search forwards within the same rule
braceLevel = 0;
for (let i = lineIndex; i < lines.length; i++) {
const line = lines[i];
for (const char of line) {
if (char === "{")
braceLevel++;
else if (char === "}")
braceLevel--;
}
const fontFamilyMatch = line.match(/font-family:\s*var\(\s*(-{2}fontfamily_[a-zA-Z0-9_]+)\s*\)/);
if (fontFamilyMatch) {
return fontFamilyMatch[1];
}
// Stop if we've left the current rule
if (braceLevel < 0) {
break;
}
}
return null;
}
/**
* Find font-style value in the current CSS rule context
*/
findFontStyleInRule(lines, lineIndex) {
// Similar logic to findFontFamilyInRule but looking for font-style
let braceLevel = 0;
let foundOpenBrace = false;
// Search in both directions within the CSS rule
for (let i = Math.max(0, lineIndex - 5); i <= Math.min(lines.length - 1, lineIndex + 5); i++) {
const line = lines[i];
const fontStyleMatch = line.match(/font-style:\s*(normal|italic|oblique);?/i);
if (fontStyleMatch) {
return fontStyleMatch[1].toLowerCase();
}
}
return "normal"; // Default to normal if no font-style specified
}
/**
* Check if weight/style combination is valid for a specific font
*/
isValidWeightStyleCombination(projectConfig, fontFamilyVar, weight, style = "normal") {
const availableWeights = this.getDetailedAvailableWeights(projectConfig, fontFamilyVar);
// Convert weight to numeric if it's a keyword
const numericWeight = this.convertWeightToNumeric(weight);
const normalizedStyle = style.toLowerCase();
// Check if this exact weight/style combination exists
const weightKey = normalizedStyle === "italic" ? `${numericWeight}_italic` : `${numericWeight}`;
const isAvailable = availableWeights.has(weightKey);
return {
isValid: isAvailable,
availableWeights: Array.from(availableWeights.keys()),
};
}
/**
* Get detailed available weights with style information
*/
getDetailedAvailableWeights(projectConfig, fontFamilyVar) {
const weights = new Set();
// Check package-based fonts
if (projectConfig.fontConfig) {
for (const role of Object.values(projectConfig.fontConfig.roles)) {
if (role.variables?.family === fontFamilyVar && role.availableWeights) {
Object.entries(role.availableWeights).forEach(([key, weightInfo]) => {
if (weightInfo) {
// Only add non-null weights
weights.add(key);
}
});
}
}
}
// Check local fonts
if (projectConfig.localFontConfig?.detailedRoles) {
for (const role of Object.values(projectConfig.localFontConfig.detailedRoles)) {
if (role.variables?.family === fontFamilyVar && role.availableWeights) {
Object.entries(role.availableWeights).forEach(([key, weightInfo]) => {
if (weightInfo) {
weights.add(key);
}
});
}
}
}
return weights;
}
/**
* Get available weights for display in error messages
*/
getAvailableWeightsForFont(projectConfig, fontFamilyVar) {
const detailedWeights = this.getDetailedAvailableWeights(projectConfig, fontFamilyVar);
// Convert internal keys to user-friendly format
return Array.from(detailedWeights)
.map((key) => {
if (key.endsWith("_italic")) {
return `${key.replace("_italic", "")} italic`;
}
return key;
})
.sort();
}
/**
* Convert CSS weight keywords to numeric values
*/
convertWeightToNumeric(weight) {
const weightMap = {
normal: "400",
bold: "700",
};
return weightMap[weight.toLowerCase()] || weight;
}
/**
* Check if a font-weight value is valid
*/
isValidFontWeight(weight) {
const cleanWeight = weight.toLowerCase().trim();
// Check against valid CSS font-weight values
if (this.VALID_FONT_WEIGHTS.includes(cleanWeight)) {
return true;
}
// Check numeric values (100-900)
const numericWeight = parseInt(cleanWeight, 10);
if (!isNaN(numericWeight) && numericWeight >= 100 && numericWeight <= 900 && numericWeight % 100 === 0) {
return true;
}
return false;
}
/**
* Get or load project configuration for a file
*/
async getProjectConfig(filePath) {
const cacheKey = this.getProjectCacheKey(filePath);
if (this.projectConfigs.has(cacheKey)) {
return this.projectConfigs.get(cacheKey);
}
const projectConfig = await this.configDetector.detectFontConfig(filePath);
this.projectConfigs.set(cacheKey, projectConfig);
return projectConfig;
}
/**
* Generate cache key for project configuration
*/
getProjectCacheKey(filePath) {
// Use directory path as cache key to share config across files in same project
const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath));
if (workspaceFolder) {
return workspaceFolder.uri.fsPath;
}
return filePath;
}
/**
* Check if document should be validated
*/
shouldValidateDocument(document) {
const fileName = document.fileName;
// Exclude certain directories
if (fileName.includes("node_modules") || fileName.includes(".vscode") || fileName.includes("dist")) {
return false;
}
const baseName = fileName.split(/[/\\]/).pop() || "";
// CSS Modules
if (baseName.endsWith(".module.css") || baseName.endsWith(".module.scss") || baseName.endsWith(".module.less")) {
return true;
}
// Global CSS files
if (baseName === "globals.css" || baseName === "global.css" || baseName === "global.scss") {
return true;
}
// Files in styles directories
if (fileName.includes("/styles/") && (baseName.endsWith(".css") || baseName.endsWith(".scss"))) {
return true;
}
return false;
}
/**
* Generate suggestions for invalid font-family variables
*/
getSuggestions(invalid, validSet) {
const validArray = Array.from(validSet);
const suggestions = [];
// Simple similarity matching
for (const valid of validArray) {
if (this.calculateSimilarity(invalid, valid) > 0.6) {
suggestions.push(valid);
}
}
return suggestions.slice(0, 3); // Limit to 3 suggestions
}
/**
* Calculate similarity between two strings
*/
calculateSimilarity(str1, str2) {
const longer = str1.length > str2.length ? str1 : str2;
const shorter = str1.length > str2.length ? str2 : str1;
if (longer.length === 0)
return 1.0;
const distance = this.levenshteinDistance(longer, shorter);
return (longer.length - distance) / longer.length;
}
/**
* Calculate Levenshtein distance
*/
levenshteinDistance(str1, str2) {
const matrix = [];
for (let i = 0; i <= str2.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= str1.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
}
else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
}
}
}
return matrix[str2.length][str1.length];
}
/**
* Provide hover information
*/
async provideHover(document, position) {
const range = document.getWordRangeAtPosition(position, /var\([^)]+\)/);
if (!range)
return undefined;
const text = document.getText(range);
const variableMatch = text.match(/var\(\s*(-{2}fontfamily_[a-zA-Z0-9_]+)\s*\)/);
if (!variableMatch)
return undefined;
const variableName = variableMatch[1];
const projectConfig = await this.getProjectConfig(document.fileName);
if (!projectConfig.hasFontConfig && !projectConfig.hasLocalFontConfig) {
return undefined;
}
const availableWeights = this.configDetector.getAvailableWeights(projectConfig, variableName);
const supportsItalics = this.configDetector.supportsItalics(projectConfig, variableName);
if (availableWeights.length === 0) {
return undefined;
}
const markdown = new vscode.MarkdownString();
markdown.appendMarkdown(`**${variableName}**\n\n`);
markdown.appendMarkdown(`**Available weights:** ${availableWeights.join(", ")}\n\n`);
markdown.appendMarkdown(`**Italics:** ${supportsItalics ? "Yes" : "No"}\n\n`);
markdown.appendMarkdown(`**Usage:**\n`);
markdown.appendCodeblock(`font-family: var(${variableName});\nfont-weight: 400; /* or any available weight */`, "css");
return new vscode.Hover(markdown, range);
}
/**
* Clear cache and refresh configurations
*/
refreshConfig() {
this.projectConfigs.clear();
// Configuration cache cleared
}
/**
* Cleanup document validation
*/
cleanupDocument(document) {
const timeout = this.validationTimeouts.get(document.uri.toString());
if (timeout) {
clearTimeout(timeout);
this.validationTimeouts.delete(document.uri.toString());
}
this.diagnosticCollection.delete(document.uri);
}
/**
* Dispose validator resources
*/
dispose() {
this.diagnosticCollection.dispose();
this.configDetector.dispose();
// Clear all timeouts
for (const timeout of this.validationTimeouts.values()) {
clearTimeout(timeout);
}
this.validationTimeouts.clear();
}
}
exports.UnifiedFontValidator = UnifiedFontValidator;
//# sourceMappingURL=unifiedValidator.js.map
;