#!/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('')) {
this.logWarning(file, '缺少 聲明');
}
// 檢查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 = /
]*>/g;
let match;
while ((match = imgRegex.exec(content)) !== null) {
if (!match[0].includes('alt=')) {
this.logError(file, '圖片缺少 alt 屬性(無障礙性要求)');
}
}
// 檢查表單標籤
const inputRegex = /]*>/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, `輸入框缺少對應的