dramaling-vocab-learning/docs/02_design/design-system/automation/component-validator.js

381 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Drama Ling 元件驗證工具
* 功能驗證HTML元件是否符合設計規範
* 作者Drama Ling 開發團隊
* 日期2025-09-15
*/
const fs = require('fs');
const path = require('path');
// ANSI 顏色碼
const colors = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
reset: '\x1b[0m'
};
// 設計規範定義
const DESIGN_SPECS = {
// 必須使用的CSS類別前綴
classPrefixes: ['btn-', 'card-', 'input-', 'alert-', 'badge-', 'modal-'],
// 必須包含的屬性
requiredAttributes: {
'button': ['type', 'class'],
'input': ['type', 'id', 'name'],
'img': ['alt', 'src'],
'a': ['href']
},
// 顏色變數
colorVariables: [
'--primary', '--primary-dark', '--primary-light',
'--secondary', '--secondary-dark', '--secondary-light',
'--success', '--warning', '--danger', '--info',
'--gray-50', '--gray-100', '--gray-200', '--gray-300',
'--gray-400', '--gray-500', '--gray-600', '--gray-700',
'--gray-800', '--gray-900'
],
// 間距變數
spacingVariables: [
'--space-1', '--space-2', '--space-3', '--space-4',
'--space-5', '--space-6', '--space-8', '--space-10'
]
};
class ComponentValidator {
constructor() {
this.errors = [];
this.warnings = [];
this.passed = 0;
this.failed = 0;
}
// 日誌方法
logError(file, message) {
this.errors.push({ file, message });
console.log(`${colors.red}[ERROR]${colors.reset} ${file}: ${message}`);
}
logWarning(file, message) {
this.warnings.push({ file, message });
console.log(`${colors.yellow}[WARNING]${colors.reset} ${file}: ${message}`);
}
logSuccess(message) {
console.log(`${colors.green}[✓]${colors.reset} ${message}`);
}
// 驗證HTML文件
validateHTMLFile(filePath) {
const fileName = path.basename(filePath);
console.log(`\n檢查文件: ${fileName}`);
try {
const content = fs.readFileSync(filePath, 'utf8');
// 檢查基本結構
this.checkHTMLStructure(fileName, content);
// 檢查必要屬性
this.checkRequiredAttributes(fileName, content);
// 檢查CSS類別命名
this.checkCSSClasses(fileName, content);
// 檢查無障礙性
this.checkAccessibility(fileName, content);
// 檢查響應式設計
this.checkResponsive(fileName, content);
this.passed++;
this.logSuccess(`${fileName} 驗證通過`);
} catch (error) {
this.failed++;
this.logError(fileName, `無法讀取文件: ${error.message}`);
}
}
// 檢查HTML基本結構
checkHTMLStructure(file, content) {
// 檢查DOCTYPE
if (!content.includes('<!DOCTYPE html>')) {
this.logWarning(file, '缺少 <!DOCTYPE html> 聲明');
}
// 檢查meta viewport
if (!content.includes('viewport')) {
this.logError(file, '缺少 viewport meta 標籤(響應式設計必需)');
}
// 檢查字符編碼
if (!content.includes('charset="UTF-8"') && !content.includes('charset=UTF-8')) {
this.logError(file, '缺少 UTF-8 字符編碼聲明');
}
}
// 檢查必要屬性
checkRequiredAttributes(file, content) {
for (const [element, attributes] of Object.entries(DESIGN_SPECS.requiredAttributes)) {
const regex = new RegExp(`<${element}[^>]*>`, 'gi');
const matches = content.match(regex) || [];
matches.forEach(match => {
attributes.forEach(attr => {
if (!match.includes(attr)) {
this.logWarning(file, `<${element}> 元素缺少 ${attr} 屬性`);
}
});
});
}
}
// 檢查CSS類別命名規範
checkCSSClasses(file, content) {
const classRegex = /class="([^"]*)"/g;
let match;
while ((match = classRegex.exec(content)) !== null) {
const classes = match[1].split(' ');
classes.forEach(className => {
// 檢查是否使用 BEM 命名或設計系統前綴
const isValidClass =
DESIGN_SPECS.classPrefixes.some(prefix => className.startsWith(prefix)) ||
className.includes('__') || // BEM element
className.includes('--'); // BEM modifier
if (!isValidClass && className && !className.startsWith('library-') && !className.startsWith('showcase-')) {
this.logWarning(file, `CSS類別 "${className}" 可能不符合命名規範`);
}
});
}
}
// 檢查無障礙性
checkAccessibility(file, content) {
// 檢查圖片alt屬性
const imgRegex = /<img[^>]*>/g;
let match;
while ((match = imgRegex.exec(content)) !== null) {
if (!match[0].includes('alt=')) {
this.logError(file, '圖片缺少 alt 屬性(無障礙性要求)');
}
}
// 檢查表單標籤
const inputRegex = /<input[^>]*>/g;
const inputs = content.match(inputRegex) || [];
inputs.forEach(input => {
if (!input.includes('type="hidden"') && !input.includes('aria-label')) {
// 檢查是否有對應的label
const idMatch = input.match(/id="([^"]*)"/);
if (idMatch) {
const hasLabel = content.includes(`for="${idMatch[1]}"`);
if (!hasLabel) {
this.logWarning(file, `輸入框缺少對應的 <label> 標籤`);
}
}
}
});
// 檢查ARIA屬性
if (content.includes('role="button"') && !content.includes('tabindex')) {
this.logWarning(file, '具有 role="button" 的元素應該包含 tabindex 屬性');
}
}
// 檢查響應式設計
checkResponsive(file, content) {
// 檢查是否使用響應式單位
const hasResponsiveUnits =
content.includes('rem') ||
content.includes('em') ||
content.includes('%') ||
content.includes('vw') ||
content.includes('vh');
if (!hasResponsiveUnits) {
this.logWarning(file, '未檢測到響應式單位rem, em, %, vw, vh');
}
// 檢查媒體查詢
if (!content.includes('@media')) {
this.logWarning(file, '未檢測到媒體查詢(響應式設計)');
}
}
// 驗證CSS文件
validateCSSFile(filePath) {
const fileName = path.basename(filePath);
console.log(`\n檢查CSS文件: ${fileName}`);
try {
const content = fs.readFileSync(filePath, 'utf8');
// 檢查設計代幣使用
this.checkDesignTokens(fileName, content);
// 檢查顏色變數
this.checkColorVariables(fileName, content);
// 檢查間距變數
this.checkSpacingVariables(fileName, content);
this.passed++;
this.logSuccess(`${fileName} CSS驗證通過`);
} catch (error) {
this.failed++;
this.logError(fileName, `無法讀取CSS文件: ${error.message}`);
}
}
// 檢查設計代幣
checkDesignTokens(file, content) {
// 檢查是否使用CSS變數而非硬編碼值
const hardcodedColors = content.match(/#[0-9a-fA-F]{3,6}/g) || [];
if (hardcodedColors.length > 5) {
this.logWarning(file, `發現 ${hardcodedColors.length} 個硬編碼顏色值建議使用CSS變數`);
}
// 檢查硬編碼的間距
const hardcodedSpacing = content.match(/margin:\s*\d+px|padding:\s*\d+px/g) || [];
if (hardcodedSpacing.length > 10) {
this.logWarning(file, `發現 ${hardcodedSpacing.length} 個硬編碼間距值,建議使用間距變數`);
}
}
// 檢查顏色變數
checkColorVariables(file, content) {
const unusedColors = DESIGN_SPECS.colorVariables.filter(
color => !content.includes(color)
);
if (unusedColors.length > 0 && unusedColors.length < DESIGN_SPECS.colorVariables.length / 2) {
this.logWarning(file, `未使用的顏色變數: ${unusedColors.slice(0, 5).join(', ')}...`);
}
}
// 檢查間距變數
checkSpacingVariables(file, content) {
const hasSpacingVars = DESIGN_SPECS.spacingVariables.some(
spacing => content.includes(spacing)
);
if (!hasSpacingVars) {
this.logWarning(file, '未使用間距變數,建議使用統一的間距系統');
}
}
// 生成報告
generateReport() {
const reportPath = path.join(__dirname, '../VALIDATION_REPORT.md');
const timestamp = new Date().toISOString().replace('T', ' ').substr(0, 19);
let report = `# 元件驗證報告\n\n`;
report += `**生成時間**: ${timestamp}\n\n`;
report += `## 📊 驗證統計\n\n`;
report += `- ✅ 通過: ${this.passed} 個文件\n`;
report += `- ❌ 失敗: ${this.failed} 個文件\n`;
report += `- ⚠️ 警告: ${this.warnings.length}\n`;
report += `- 🚨 錯誤: ${this.errors.length}\n\n`;
if (this.errors.length > 0) {
report += `## 🚨 錯誤列表\n\n`;
this.errors.forEach(({ file, message }) => {
report += `- **${file}**: ${message}\n`;
});
report += '\n';
}
if (this.warnings.length > 0) {
report += `## ⚠️ 警告列表\n\n`;
this.warnings.forEach(({ file, message }) => {
report += `- **${file}**: ${message}\n`;
});
report += '\n';
}
report += `## 📝 建議\n\n`;
report += `1. 修復所有錯誤以確保符合設計規範\n`;
report += `2. 檢查警告並根據需要進行調整\n`;
report += `3. 使用設計代幣取代硬編碼值\n`;
report += `4. 確保所有元件都有適當的無障礙性支援\n`;
fs.writeFileSync(reportPath, report);
console.log(`\n📋 驗證報告已生成: ${reportPath}`);
}
// 執行驗證
run() {
console.log('=====================================');
console.log('Drama Ling 元件驗證工具');
console.log('=====================================\n');
const componentLibraryPath = path.join(__dirname, '../../component-library');
// 驗證HTML文件
this.validateDirectory(componentLibraryPath, '.html', this.validateHTMLFile.bind(this));
// 驗證CSS文件
const cssPath = path.join(componentLibraryPath, 'assets/styles');
this.validateDirectory(cssPath, '.css', this.validateCSSFile.bind(this));
// 生成報告
this.generateReport();
console.log('\n=====================================');
console.log('驗證完成!');
console.log('=====================================');
// 返回退出碼
process.exit(this.errors.length > 0 ? 1 : 0);
}
// 驗證目錄中的文件
validateDirectory(dirPath, extension, validateFunc) {
if (!fs.existsSync(dirPath)) {
this.logError('系統', `目錄不存在: ${dirPath}`);
return;
}
const files = this.getAllFiles(dirPath, extension);
files.forEach(file => validateFunc(file));
}
// 遞歸獲取所有文件
getAllFiles(dirPath, extension) {
let files = [];
const items = fs.readdirSync(dirPath);
items.forEach(item => {
const fullPath = path.join(dirPath, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
files = files.concat(this.getAllFiles(fullPath, extension));
} else if (path.extname(fullPath) === extension) {
files.push(fullPath);
}
});
return files;
}
}
// 執行驗證
const validator = new ComponentValidator();
validator.run();