UNPKG

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
"use strict"; 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