w1-system-font-validator
Version:
VS Code extension for validating W1 System font variables (both fontConfig.json and localFontConfig.json)
224 lines (189 loc) • 7.44 kB
text/typescript
import * as fs from "fs";
import * as path from "path";
import * as vscode from "vscode";
import { FontConfig, LocalFontConfig, ProjectFontConfig, WeightInfo } from "../types/fontConfig";
export class ConfigDetector {
private configWatcher: vscode.FileSystemWatcher | null = null;
constructor() {
this.setupConfigWatcher();
}
/**
* Find project root by looking for project boundary markers
*/
private findProjectRoot(filePath: string): string | null {
let currentDir = path.dirname(filePath);
const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath));
const workspaceRoot = workspaceFolder?.uri.fsPath;
while (currentDir !== path.dirname(currentDir)) {
// Stop at workspace boundary
if (workspaceRoot && currentDir === workspaceRoot) {
const projectMarkers = ["package.json", ".git", "next.config.js", "tsconfig.json"];
for (const marker of projectMarkers) {
if (fs.existsSync(path.join(currentDir, marker))) {
return currentDir;
}
}
break;
}
// Check for project boundary markers
const projectMarkers = ["package.json", ".git", "next.config.js", "tsconfig.json"];
for (const marker of projectMarkers) {
if (fs.existsSync(path.join(currentDir, marker))) {
return currentDir;
}
}
currentDir = path.dirname(currentDir);
}
return null;
}
/**
* Detect and load font configurations for a specific file
*/
public async detectFontConfig(filePath: string): Promise<ProjectFontConfig> {
const projectRoot = this.findProjectRoot(filePath);
const result: ProjectFontConfig = {
hasFontConfig: false,
hasLocalFontConfig: false,
};
if (!projectRoot) {
// No project root found
return result;
}
// Check for fontConfig.json (package-based fonts)
const fontConfigPath = path.join(projectRoot, "fontConfig.json");
if (fs.existsSync(fontConfigPath)) {
try {
const fontConfigContent = fs.readFileSync(fontConfigPath, "utf8");
const fontConfig = JSON.parse(fontConfigContent) as FontConfig;
result.hasFontConfig = true;
result.fontConfigPath = fontConfigPath;
result.fontConfig = fontConfig;
// fontConfig.json loaded successfully
} catch (error) {
console.error(`[W1 Font Validator] Error loading fontConfig.json:`, error);
}
}
// Check for localFontConfig.json (local file-based fonts)
const localFontConfigPath = path.join(projectRoot, "localFontConfig.json");
if (fs.existsSync(localFontConfigPath)) {
try {
const localFontConfigContent = fs.readFileSync(localFontConfigPath, "utf8");
const localFontConfig = JSON.parse(localFontConfigContent) as LocalFontConfig;
result.hasLocalFontConfig = true;
result.localFontConfigPath = localFontConfigPath;
result.localFontConfig = localFontConfig;
// localFontConfig.json loaded successfully
} catch (error) {
console.error(`[W1 Font Validator] Error loading localFontConfig.json:`, error);
}
}
return result;
}
/**
* Get all valid font-family variables from both configurations
*/
public getValidFontFamilies(projectConfig: ProjectFontConfig): Set<string> {
const validFamilies = new Set<string>();
// Add package-based font families
if (projectConfig.fontConfig) {
Object.values(projectConfig.fontConfig.roles).forEach((role) => {
if (role.variables?.family) {
validFamilies.add(role.variables.family);
}
});
}
// Add local font families
if (projectConfig.localFontConfig) {
if (projectConfig.localFontConfig.detailedRoles) {
Object.values(projectConfig.localFontConfig.detailedRoles).forEach((role) => {
if (role.variables?.family) {
validFamilies.add(role.variables.family);
}
});
}
// Also add from validVariables array (fallback)
projectConfig.localFontConfig.validVariables?.forEach((variable) => {
if (variable.startsWith("--fontfamily_")) {
validFamilies.add(variable);
}
});
}
return validFamilies;
}
/**
* Get available weights for a specific font-family variable
*/
public getAvailableWeights(projectConfig: ProjectFontConfig, fontFamilyVar: string): string[] {
// Check package-based fonts
if (projectConfig.fontConfig) {
for (const role of Object.values(projectConfig.fontConfig.roles)) {
if (role.variables?.family === fontFamilyVar) {
// Extract weight keys from the availableWeights object
return Object.keys(role.availableWeights || {}).filter((key) => role.availableWeights[key] !== null);
}
}
}
// Check local fonts (now uses same object format as package fonts)
if (projectConfig.localFontConfig?.detailedRoles) {
for (const role of Object.values(projectConfig.localFontConfig.detailedRoles)) {
if (role.variables?.family === fontFamilyVar) {
// Same object format: {"400": {...}, "400_italic": {...}}
const weightsObject = role.availableWeights as Record<string, WeightInfo | null>;
return Object.keys(weightsObject || {}).filter((key) => weightsObject[key] !== null);
}
}
}
return [];
}
/**
* Check if a font-family variable supports italics
*/
public supportsItalics(projectConfig: ProjectFontConfig, fontFamilyVar: string): boolean {
// Check package-based fonts
if (projectConfig.fontConfig) {
for (const role of Object.values(projectConfig.fontConfig.roles)) {
if (role.variables?.family === fontFamilyVar) {
return role.hasItalics || false;
}
}
}
// Check local fonts
if (projectConfig.localFontConfig?.detailedRoles) {
for (const role of Object.values(projectConfig.localFontConfig.detailedRoles)) {
if (role.variables?.family === fontFamilyVar) {
return role.hasItalics || false;
}
}
}
return false;
}
/**
* Setup file system watchers for configuration changes
*/
private setupConfigWatcher(): void {
this.configWatcher = vscode.workspace.createFileSystemWatcher("**/{fontConfig.json,localFontConfig.json}");
this.configWatcher.onDidChange((uri: vscode.Uri) => {
// Font config changed, will refresh
// Trigger re-validation of affected documents
this.onConfigChanged();
});
this.configWatcher.onDidCreate((uri: vscode.Uri) => {
// Font config created, will refresh
this.onConfigChanged();
});
this.configWatcher.onDidDelete((uri: vscode.Uri) => {
// Font config deleted, will refresh
this.onConfigChanged();
});
}
private onConfigChanged(): void {
// Clear any cached configurations and trigger re-validation
// This will be implemented in the main validator
vscode.commands.executeCommand("w1FontValidator.refreshConfig");
}
public dispose(): void {
if (this.configWatcher) {
this.configWatcher.dispose();
}
}
}