vue-cssgen
Version:
Инструмент для автоматизации работы со стилями в Vue 3-проектах
307 lines (296 loc) • 10.6 kB
JavaScript
// 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 = `
<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, "<")
.replace(/>/g, ">");
}
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');