UNPKG

vue-cssgen

Version:

Инструмент для автоматизации работы со стилями в Vue 3-проектах

307 lines (296 loc) 10.6 kB
// scripts/export-ui.js import fs from 'fs'; import path from 'path'; // Импорт дефолтных правил напрямую! import defaultRules from '../rules/rules.js'; // -- Генерация JS-файла с дефолтными правилами -- const rulesDir = path.resolve(process.cwd(), 'vue-cssgen-rules'); if (!fs.existsSync(rulesDir)) fs.mkdirSync(rulesDir, { recursive: true }); function serializeRule( rule ) { const out = { ...rule }; if (rule.match instanceof RegExp) { out.match = rule.match.toString(); } if (typeof rule.css === 'function' && out.examples && out.examples.length) { try { out.cssResult = rule.css(out.examples[0]); } catch (e) { out.cssResult = '[ошибка выполнения]'; } } else if (typeof rule.css === 'string') { out.cssResult = rule.css; } delete out.css; return out; } const rulesExport = ` export default [ ${defaultRules.map(rule => ' ' + JSON.stringify(serializeRule(rule))).join(',\n')} ]; `.trim(); fs.writeFileSync(path.join(rulesDir, 'default-rules.js'), rulesExport, 'utf8'); // -- Генерация ui.html -- const html = ` <!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>vue-cssgen: справочник автоклассов</title> <style> body { background: #171e2b; color: #e2e8f0; font-family: Inter, sans-serif; margin: 0; } .wrap { max-width: 1600px; margin: 0 auto; padding: 24px 32px 64px 32px; position: relative; } .rules-header, .rules-row { display: grid; grid-template-columns: 2fr 3fr 1.1fr 1.1fr 2.2fr; align-items: stretch; } .rules-header { background: #232b39; color: #81aaff; font-weight: 600; font-size: 16px; border-radius: 10px 10px 0 0; border: 1px solid #374151; border-bottom: none; min-height: 44px; } .rules-row { background: #232b39; border-left: 1px solid #374151; border-right: 1px solid #374151; border-bottom: 1px solid #374151; font-size: 15px; min-height: 36px; } .rules-row:nth-child(even) { background: #202736; } .rules-cell { padding: 10px 12px; border-right: 1px solid #374151; word-break: break-word; white-space: pre-line; overflow-wrap: anywhere; max-width: 100%; display: block; } .rules-header > div, .rules-row > div:last-child { border-right: none; } code, pre { background: #232b39; color: #facc15; padding: 1px 5px; border-radius: 6px; max-width: 100%; display: block; word-break: break-word; white-space: pre-line; overflow-x: auto; } pre { margin: 0; font-size: 13px; color: #80ffbe; background: transparent; } .filter-bar { position: absolute; top: 24px; right: 32px; display: flex; align-items: center; gap: 10px; } .filter-bar input[type="text"] { background: #232b39; border: 1px solid #374151; color: #e2e8f0; border-radius: 7px; padding: 7px 13px; font-size: 15px; outline: none; transition: border .2s; } .filter-bar input[type="text"]:focus { border-color: #7c3aed; } .examples-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 16px; align-items: start; max-width: 100%; } .example-item { overflow-x: auto; text-overflow: ellipsis; padding: 0; margin: 0; min-width: 0; white-space: pre-line; } .examples-grid code { display: inline-block; vertical-align: middle; background: #232b39; color: #facc15; padding: 1px 5px; border-radius: 6px; font-size: 14px; margin-right: 5px; max-width: 95%; word-break: break-all; white-space: pre; } @media (max-width:1100px) { .wrap { padding: 4px 2px; } .rules-header, .rules-row { font-size: 12px; grid-template-columns: 2fr 2fr 1.3fr 1.3fr 2fr; } .rules-cell { padding: 5px 6px; } } @media (max-width:900px) { .examples-grid { grid-template-columns: 1fr; } } @media (max-width:600px) { .wrap { padding: 2px 0; } .filter-bar { position: static; margin-bottom: 14px; } .rules-header, .rules-row { grid-template-columns: 1fr; display: block; font-size: 11px; } .rules-row, .rules-header { border-radius: 0; } } </style> </head> <body> <div class="wrap"> <div class="filter-bar"> <input id="filter" type="text" placeholder="Быстрый поиск по классам..." autocomplete="off" /> </div> <h1>Справочник автоклассов (vue-cssgen)</h1> <div id="rules-table"></div> </div> <script type="module"> import defaultRules from './default-rules.js'; let customRules = []; try { const mod = await import('./project-rules.js'); customRules = mod.default || []; } catch(e) { customRules = []; } for (const rule of customRules) { if (typeof rule.css === 'function' && rule.examples && rule.examples.length) { try { rule.cssResult = rule.css(rule.examples[0]); } catch (e) { rule.cssResult = '[ошибка выполнения]'; } } else if (typeof rule.css === 'string') { rule.cssResult = rule.css; } } function strToRegExp(str) { if (typeof str !== 'string') return str; const match = str.match(/^\\/(.*)\\/([gimsuy]*)$/); if (match) { return new RegExp(match[1], match[2]); } return str; } function regExpToString(regexp) { if (typeof regexp === 'string') return regexp; return regexp && regexp.toString(); } const defaultMap = new Map(defaultRules.map(r => [regExpToString(strToRegExp(r.match)), r])); const customMap = new Map(customRules.map(r => [regExpToString(strToRegExp(r.match)), r])); function mergeRules(defaultRules, customRules) { const ruleMap = new Map(); for (const rule of defaultRules) { rule.match = strToRegExp(rule.match); ruleMap.set(regExpToString(rule.match), rule); } for (const rule of customRules) { rule.match = strToRegExp(rule.match); ruleMap.set(regExpToString(rule.match), rule); } return Array.from(ruleMap.values()); } const rules = mergeRules(defaultRules, customRules); function escapeHtml(str) { return String(str ?? '') .replace(/</g, "&lt;") .replace(/>/g, "&gt;"); } function getSource(rule) { const matchStr = regExpToString(rule.match); if (customMap.has(matchStr)) { return defaultMap.has(matchStr) ? 'кастомный (заменяет дефолтный)' : 'кастомный'; } else { return 'дефолтный'; } } function renderExamples(rule) { let items = []; if (rule.values && typeof rule.values === 'object' && Object.keys(rule.values).length > 0) { items = Object.entries(rule.values) .map(([key, val]) => '<div class="example-item"><code>' + escapeHtml((rule.prefix || '') + key) + '</code> <span style="color:#a5ffb9;">' + escapeHtml(val) + '</span></div>' ); } else if (rule.values === null) { if (rule.examples && rule.examples.length) items = rule.examples.map(e => '<div class="example-item"><code>' + escapeHtml(e) + '</code></div>'); else items = ['<span style="color:#f9a;">Динамический класс</span>']; } else if (rule.examples && rule.examples.length) { items = rule.examples.map(e => '<div class="example-item"><code>' + escapeHtml(e) + '</code></div>'); } return \`<div class="examples-grid">\${items.join('')}</div>\`; } function renderGrid(rules, filter = '') { const lower = filter.trim().toLowerCase(); const filtered = lower ? rules.filter(rule => { return ( (rule.match && regExpToString(rule.match).toLowerCase().includes(lower)) || (rule.examples && rule.examples.join(',').toLowerCase().includes(lower)) || (rule.desc && rule.desc.toLowerCase().includes(lower)) || (rule.cssResult && rule.cssResult.toLowerCase().includes(lower)) ); }) : rules; return \` <div class="rules-header"> <div>Регулярка (match)</div> <div>Пример (examples)</div> <div>Описание (desc)</div> <div>Источник</div> <div>CSS-результат</div> </div> <div> \${filtered.map(rule => \` <div class="rules-row"> <div class="rules-cell"><code>\${escapeHtml(regExpToString(rule.match))}</code></div> <div class="rules-cell">\${renderExamples(rule)}</div> <div class="rules-cell">\${escapeHtml(rule.desc || '')}</div> <div class="rules-cell">\${getSource(rule)}</div> <div class="rules-cell"><pre>\${escapeHtml(rule.cssResult || '')}</pre></div> </div> \`).join('')} </div> \`; } const $table = document.getElementById('rules-table'); const $input = document.getElementById('filter'); function rerender() { $table.innerHTML = renderGrid(rules, $input.value); } $input.addEventListener('input', rerender); rerender(); </script> </body> </html> `.trim(); fs.writeFileSync(path.join(rulesDir, 'ui.html'), html, 'utf8'); console.log('Файлы сгенерированы:'); console.log('- vue-cssgen-rules/default-rules.js'); console.log('- vue-cssgen-rules/ui.html');