feat: 规则转换器/导入/过滤/i18n 基础设施
- converters: 8 个转换器(markdown/txt/excel/docx/pptx/yaml + prompt-builder + converter 接口) - import: import-preview/import-service/import-types - rule-filter: 规则预过滤功能 - static-rules.json: 静态规则数据 - i18n/messages.ts: 国际化消息 - types/mammoth.d.ts: mammoth 类型声明
This commit is contained in:
@@ -0,0 +1,593 @@
|
||||
import * as vscode from 'vscode';
|
||||
import type { ConversionResult, PreviewDecision, ImportableRule } from './import-types';
|
||||
import { t } from '../i18n/messages';
|
||||
|
||||
export async function showImportPreview(
|
||||
result: ConversionResult,
|
||||
): Promise<PreviewDecision | null> {
|
||||
return new Promise((resolve) => {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'ruleImportPreview',
|
||||
t('importPreview.title'),
|
||||
vscode.ViewColumn.Active,
|
||||
{ enableScripts: true },
|
||||
);
|
||||
|
||||
const keepRule: Record<string, boolean> = {};
|
||||
for (const rule of result.rules) {
|
||||
keepRule[rule.id] = rule.duplicateLevel !== 'exact';
|
||||
}
|
||||
|
||||
panel.webview.html = renderPreviewHtml(result, keepRule);
|
||||
|
||||
panel.webview.onDidReceiveMessage((msg) => {
|
||||
if (msg.type === 'toggleRule') {
|
||||
keepRule[msg.ruleId] = msg.keep;
|
||||
} else if (msg.type === 'confirm') {
|
||||
resolve({
|
||||
keepRule,
|
||||
confirmed: true,
|
||||
editedRules: msg.editedRules as ImportableRule[] | undefined,
|
||||
});
|
||||
panel.dispose();
|
||||
} else if (msg.type === 'cancel') {
|
||||
resolve(null);
|
||||
panel.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
panel.onDidDispose(() => resolve(null));
|
||||
});
|
||||
}
|
||||
|
||||
const SEVERITY_OPTIONS = ['error', 'warning', 'info'];
|
||||
const SEVERITY_COLORS: Record<string, string> = {
|
||||
error: '#f48771',
|
||||
warning: '#d29922',
|
||||
info: '#58a6ff',
|
||||
};
|
||||
|
||||
function renderPreviewHtml(
|
||||
result: ConversionResult,
|
||||
keepRule: Record<string, boolean>,
|
||||
): string {
|
||||
const exactRules = result.rules.filter(r => r.duplicateLevel === 'exact');
|
||||
const overlapRules = result.rules.filter(r => r.duplicateLevel === 'overlap');
|
||||
const noneRules = result.rules.filter(
|
||||
r => r.duplicateLevel !== 'exact' && r.duplicateLevel !== 'overlap'
|
||||
);
|
||||
|
||||
const totalKept = Object.values(keepRule).filter(Boolean).length;
|
||||
const totalCommented = Object.values(keepRule).filter(v => !v).length;
|
||||
|
||||
function renderRuleCard(rule: ImportableRule): string {
|
||||
const kept = keepRule[rule.id];
|
||||
const color = SEVERITY_COLORS[rule.severity] || '#8b949e';
|
||||
const editRule = rule;
|
||||
|
||||
let duplicateInfo = '';
|
||||
if (rule.duplicateLevel === 'exact') {
|
||||
const prefix = rule.duplicateOf?.startsWith('custom/') ? `${t('import.customRulePrefix')} ${rule.duplicateOf.slice(7)}` : (rule.duplicateOf ?? 'unknown');
|
||||
duplicateInfo = `<div style="color:#8b949e;font-size:12px;margin-top:4px;">${t('import.duplicateOf', { 0: prefix })}</div>`;
|
||||
} else if (rule.duplicateLevel === 'overlap') {
|
||||
const prefix = rule.duplicateOf?.startsWith('custom/') ? `${t('import.customRulePrefix')} ${rule.duplicateOf.slice(7)}` : (rule.duplicateOf ?? 'unknown');
|
||||
duplicateInfo = `
|
||||
<div style="color:#d29922;font-size:12px;margin-top:4px;">${t('import.overlapWith', { 0: prefix })}</div>
|
||||
${rule.duplicateReason ? `<div style="color:#8b949e;font-size:12px;margin-top:2px;">${t('import.overlapReason', { 0: rule.duplicateReason })}</div>` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
const statusBadge = rule.duplicateLevel === 'exact'
|
||||
? `<span class="badge badge-exact">${kept ? t('import.badgeRestored') : t('import.badgeWillComment')}</span>`
|
||||
: rule.duplicateLevel === 'overlap'
|
||||
? `<span class="badge badge-overlap">${kept ? t('importPreview.keep') : t('importPreview.comment')}</span>`
|
||||
: `<span class="badge badge-none">${t('importPreview.keep')}</span>`;
|
||||
|
||||
const tagValue = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.join(',') : '';
|
||||
const tagDisplay = (tags: string[] | undefined) => tags && tags.length > 0 ? tags.map(t => `<span class="tag" data-value="${t}">${t}<span class="tag-remove" data-tag="${t}">×</span></span>`).join('') : '';
|
||||
|
||||
return `
|
||||
<div class="rule-card" data-ruleid="${rule.id}">
|
||||
<div class="rule-card-header" onclick="toggleCard('${rule.id}')">
|
||||
<div class="rule-card-summary">
|
||||
<input class="rule-id-input${/^rule-\d+$/.test(rule.id) ? ' placeholder-id' : ''}" value="${rule.id}" onchange="syncId('${rule.id}', this.value)" onclick="event.stopPropagation()">
|
||||
<span class="rule-severity-tag" style="background:${color}20;color:${color};border:1px solid ${color}40;">${editRule.severity}</span>
|
||||
<span class="rule-desc-preview">${editRule.description}</span>
|
||||
</div>
|
||||
<div class="rule-card-meta">
|
||||
${statusBadge}
|
||||
<span class="expand-icon">▼</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-card-body" id="body-${rule.id}" style="display:none;">
|
||||
<div class="edit-header">
|
||||
<div class="id-row">
|
||||
<span class="id-display-label">${t('import.idLabel')}</span>
|
||||
<input class="id-display-input${/^rule-\d+$/.test(rule.id) ? ' placeholder-id' : ''}" value="${rule.id}" onchange="syncId('${rule.id}', this.value)">
|
||||
${/^rule-\d+$/.test(rule.id) ? `<span class="placeholder-hint">${t('import.placeholderIdHint')}</span>` : ''}
|
||||
</div>
|
||||
<div class="keep-toggle">
|
||||
<button class="toggle-btn ${kept ? 'active' : ''}" data-action="keep" onclick="event.stopPropagation();toggleKeep('${rule.id}', true)">${t('importPreview.keep')}</button>
|
||||
<button class="toggle-btn ${!kept ? 'active' : ''}" data-action="comment" onclick="event.stopPropagation();toggleKeep('${rule.id}', false)">${t('importPreview.comment')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${duplicateInfo}
|
||||
|
||||
<div class="edit-field">
|
||||
<label>${t('import.severityLabel')}</label>
|
||||
<select onchange="updateRule('${rule.id}','severity',this.value)">
|
||||
${SEVERITY_OPTIONS.map(s => `<option value="${s}" ${s === editRule.severity ? 'selected' : ''}>${s}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="edit-field">
|
||||
<label>${t('import.descriptionLabel')}</label>
|
||||
<textarea rows="2" onchange="updateRule('${rule.id}','description',this.value)">${editRule.description}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="edit-field">
|
||||
<label>${t('import.messageLabel')}</label>
|
||||
<textarea rows="2" onchange="updateRule('${rule.id}','message',this.value)">${editRule.message}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="edit-field">
|
||||
<label>${t('import.languagesLabel')}</label>
|
||||
<div class="tag-input-wrapper">
|
||||
<div class="tag-list" data-ruleid="${rule.id}" data-field="languages">
|
||||
${tagDisplay(editRule.languages)}
|
||||
</div>
|
||||
<input class="tag-input" data-ruleid="${rule.id}" data-field="languages" placeholder="${t('import.tagPlaceholder')}" value="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="edit-field">
|
||||
<label>${t('import.excludeLanguagesLabel')}</label>
|
||||
<div class="tag-input-wrapper">
|
||||
<div class="tag-list" data-ruleid="${rule.id}" data-field="excludeLanguages">
|
||||
${tagDisplay(editRule.excludeLanguages)}
|
||||
</div>
|
||||
<input class="tag-input" data-ruleid="${rule.id}" data-field="excludeLanguages" placeholder="${t('import.tagPlaceholder')}" value="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSection(title: string, icon: string, rules: ImportableRule[], _defaultExpanded: boolean): string {
|
||||
if (rules.length === 0) { return ''; }
|
||||
const sectionId = `section-${title.replace(/\s/g, '')}`;
|
||||
const show = rules.some(r => keepRule[r.id] !== undefined);
|
||||
return `
|
||||
<div style="margin-bottom:12px;">
|
||||
<div class="section-header" onclick="toggleSection('${sectionId}')">
|
||||
<span style="font-size:14px;">${icon}</span>
|
||||
<span class="section-title">${title}(${t('import.ruleCount', { 0: rules.length })})</span>
|
||||
<span class="section-arrow">▶</span>
|
||||
</div>
|
||||
<div id="${sectionId}" style="display:${show ? 'block' : 'none'};">
|
||||
${rules.map(renderRuleCard).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: var(--vscode-font-family);
|
||||
font-size: var(--vscode-font-size);
|
||||
color: var(--vscode-foreground);
|
||||
background: var(--vscode-editor-background);
|
||||
padding: 16px; line-height: 1.5;
|
||||
}
|
||||
.header {
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
padding-bottom: 12px; margin-bottom: 12px;
|
||||
}
|
||||
.header-title { font-size: 16px; font-weight: 700; margin-bottom: 4px; }
|
||||
.header-sub { font-size: 12px; color: var(--vscode-descriptionForeground); }
|
||||
.summary {
|
||||
display: flex; gap: 12px; margin-bottom: 16px;
|
||||
}
|
||||
.summary-item {
|
||||
padding: 8px 12px; border-radius: 6px; font-size: 12px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
background: var(--vscode-sideBar-background, var(--vscode-editor-background));
|
||||
}
|
||||
.summary-bar {
|
||||
margin-bottom: 16px; padding: 8px 12px; border-radius: 6px;
|
||||
font-size: 12px; background: rgba(139,92,246,0.1);
|
||||
border: 1px solid rgba(139,92,246,0.3); color: #a78bfa;
|
||||
}
|
||||
.actions {
|
||||
display: flex; gap: 8px; padding-top: 12px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.btn {
|
||||
padding: 6px 16px; border: 1px solid var(--vscode-panel-border);
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border-radius: 6px; cursor: pointer; font-size: 12px;
|
||||
}
|
||||
.btn:hover { background: var(--vscode-button-secondaryHoverBackground); }
|
||||
.btn-primary { background: #7c3aed; color: #fff; border-color: #7c3aed; }
|
||||
.btn-primary:hover { background: #8b5cf6; }
|
||||
.section-header {
|
||||
display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
|
||||
cursor: pointer; padding: 4px 0;
|
||||
}
|
||||
.section-title { font-weight: 600; font-size: 13px; }
|
||||
.section-arrow { font-size: 10px; color: var(--vscode-descriptionForeground); }
|
||||
.rule-card {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 8px; margin-bottom: 8px; overflow: hidden;
|
||||
}
|
||||
.rule-card-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 12px; cursor: pointer; gap: 8px;
|
||||
}
|
||||
.rule-card-header:hover { background: var(--vscode-list-hoverBackground); }
|
||||
.rule-card-summary {
|
||||
display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0;
|
||||
}
|
||||
.rule-id-input {
|
||||
font-family: monospace; font-size: 13px; font-weight: 600; white-space: nowrap;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
color: var(--vscode-foreground);
|
||||
padding: 1px 4px; outline: none;
|
||||
width: auto; min-width: 120px;
|
||||
}
|
||||
.rule-id-input:focus {
|
||||
border-color: var(--vscode-focusBorder, #7c3aed);
|
||||
background: var(--vscode-input-background);
|
||||
}
|
||||
.rule-id-input.placeholder-id {
|
||||
border-color: #f59e0b !important;
|
||||
box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
.rule-severity-tag {
|
||||
padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; white-space: nowrap;
|
||||
}
|
||||
.rule-desc-preview {
|
||||
font-size: 12px; color: var(--vscode-descriptionForeground);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.rule-card-meta {
|
||||
display: flex; align-items: center; gap: 8px; flex-shrink: 0;
|
||||
}
|
||||
.expand-icon { font-size: 10px; color: var(--vscode-descriptionForeground); }
|
||||
.badge {
|
||||
padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600;
|
||||
}
|
||||
.badge-exact { background: rgba(248,81,73,0.15); color: #f48771; }
|
||||
.badge-overlap { background: rgba(210,153,34,0.15); color: #d29922; }
|
||||
.badge-none { background: rgba(35,134,54,0.15); color: #3fb950; }
|
||||
.rule-card-body {
|
||||
padding: 0 12px 12px; border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
.edit-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 10px 0 8px;
|
||||
}
|
||||
.id-row {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.keep-toggle { display: flex; gap: 4px; }
|
||||
.toggle-btn {
|
||||
padding: 3px 12px; border-radius: 4px; cursor: pointer; font-size: 11px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
background: transparent; color: var(--vscode-foreground);
|
||||
}
|
||||
.toggle-btn.active {
|
||||
background: rgba(35,134,54,0.15); color: #3fb950; border-color: rgba(35,134,54,0.3);
|
||||
}
|
||||
.toggle-btn.active[data-action="comment"] {
|
||||
background: rgba(248,81,73,0.15); color: #f48771; border-color: rgba(248,81,73,0.3);
|
||||
}
|
||||
.edit-field { margin-top: 10px; }
|
||||
.edit-field label {
|
||||
display: block; font-size: 11px; font-weight: 600;
|
||||
color: var(--vscode-descriptionForeground); margin-bottom: 4px;
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
.id-display-label {
|
||||
font-size: 11px; font-weight: 600;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
text-transform: uppercase; letter-spacing: 0.5px;
|
||||
}
|
||||
.id-display-input {
|
||||
font-family: monospace; font-size: 13px; font-weight: 600;
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 3px;
|
||||
color: var(--vscode-input-foreground);
|
||||
padding: 4px 8px; outline: none;
|
||||
width: auto; min-width: 200px;
|
||||
}
|
||||
.id-display-input:focus {
|
||||
border-color: var(--vscode-focusBorder, #7c3aed);
|
||||
}
|
||||
.id-display-input.placeholder-id {
|
||||
border-color: #f59e0b !important;
|
||||
box-shadow: 0 0 0 1px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
.placeholder-hint {
|
||||
color: #d29922; font-size: 11px;
|
||||
}
|
||||
.edit-field select, .edit-field textarea {
|
||||
width: 100%; padding: 6px 8px; border-radius: 4px;
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
font-family: var(--vscode-font-family);
|
||||
font-size: var(--vscode-font-size);
|
||||
}
|
||||
.edit-field textarea { resize: vertical; }
|
||||
.tag-input-wrapper {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
border-radius: 4px; padding: 4px 6px;
|
||||
background: var(--vscode-input-background);
|
||||
display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
|
||||
}
|
||||
.tag-list { display: flex; flex-wrap: wrap; gap: 4px; }
|
||||
.tag {
|
||||
display: inline-flex; align-items: center; gap: 3px;
|
||||
padding: 1px 6px; border-radius: 3px; font-size: 11px;
|
||||
background: rgba(88,166,255,0.15); color: #58a6ff;
|
||||
border: 1px solid rgba(88,166,255,0.3);
|
||||
}
|
||||
.tag-remove {
|
||||
cursor: pointer; font-size: 13px; line-height: 1; opacity: 0.7;
|
||||
}
|
||||
.tag-remove:hover { opacity: 1; }
|
||||
.tag-input {
|
||||
border: none; outline: none; flex: 1; min-width: 80px;
|
||||
background: transparent; color: var(--vscode-input-foreground);
|
||||
font-size: 12px; padding: 2px 0;
|
||||
}
|
||||
.tag-input::placeholder { color: var(--vscode-input-placeholderForeground); }
|
||||
.validation-error {
|
||||
padding: 8px 12px; margin-bottom: 12px; border-radius: 6px;
|
||||
background: rgba(248,81,73,0.15); color: #f48771;
|
||||
border: 1px solid rgba(248,81,73,0.3); font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<div class="header-title">${t('importPreview.title')}</div>
|
||||
<div class="header-sub">${t('importPreview.source', { 0: result.sourceFileName, 1: String(result.rules.length) })}</div>
|
||||
</div>
|
||||
<div class="summary">
|
||||
<div class="summary-item" style="border-color:rgba(248,81,73,0.3);color:#f48771;">${t('import.exactDuplicate', { 0: exactRules.length })}</div>
|
||||
<div class="summary-item" style="border-color:rgba(210,153,34,0.3);color:#d29922;">${t('import.overlapDuplicate', { 0: overlapRules.length })}</div>
|
||||
<div class="summary-item" style="border-color:rgba(35,134,54,0.3);color:#3fb950;">${t('import.noDuplicate', { 0: noneRules.length })}</div>
|
||||
</div>
|
||||
<div class="summary-bar" id="statusBar">
|
||||
${t('import.statusBar', { 0: `<b id="keepCount">${totalKept}</b>`, 1: `<b id="commentCount">${totalCommented}</b>` })}
|
||||
<span id="editHint" style="display:none;">${t('import.editedHint', { 0: '<b id="editCount">0</b>' })}</span>
|
||||
</div>
|
||||
|
||||
<div id="validationError" class="validation-error" style="display:none;"></div>
|
||||
|
||||
${renderSection(t('import.sectionExact'), '⛔', exactRules, false)}
|
||||
${renderSection(t('import.sectionOverlap'), '⚠️', overlapRules, true)}
|
||||
${renderSection(t('import.sectionNone'), '✅', noneRules, false)}
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn" onclick="cancel()">${t('importPreview.cancel')}</button>
|
||||
<button class="btn btn-primary" onclick="doConfirm()">${t('importPreview.confirm')}</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const vscode = acquireVsCodeApi();
|
||||
const editedRules = {};
|
||||
const VALIDATION_DESC_EMPTY = ${JSON.stringify(t('import.validationDescEmpty'))};
|
||||
const VALIDATION_MSG_EMPTY = ${JSON.stringify(t('import.validationMsgEmpty'))};
|
||||
const VALIDATION_ID_EMPTY = ${JSON.stringify(t('import.validationIdEmpty'))};
|
||||
|
||||
function syncId(originalId, newValue) {
|
||||
const card = document.querySelector('.rule-card[data-ruleid="' + originalId + '"]');
|
||||
if (!card) return;
|
||||
const headerInput = card.querySelector('.rule-id-input');
|
||||
const panelInput = card.querySelector('.id-display-input');
|
||||
if (headerInput) headerInput.value = newValue;
|
||||
if (panelInput) panelInput.value = newValue;
|
||||
|
||||
if (!editedRules[originalId]) editedRules[originalId] = {};
|
||||
editedRules[originalId].id = newValue;
|
||||
updateEditHint();
|
||||
|
||||
const isPlaceholder = /^rule-\\d+$/.test(newValue);
|
||||
[headerInput, panelInput].forEach(el => {
|
||||
if (el) {
|
||||
el.classList.toggle('placeholder-id', isPlaceholder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleCard(ruleId) {
|
||||
const body = document.getElementById('body-' + ruleId);
|
||||
const card = body.closest('.rule-card');
|
||||
const icon = card.querySelector('.expand-icon');
|
||||
if (body.style.display === 'none') {
|
||||
body.style.display = 'block';
|
||||
icon.textContent = '▲';
|
||||
} else {
|
||||
body.style.display = 'none';
|
||||
icon.textContent = '▼';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSection(id) {
|
||||
const el = document.getElementById(id);
|
||||
const arrow = el.previousElementSibling.querySelector('.section-arrow');
|
||||
if (el.style.display === 'none') {
|
||||
el.style.display = 'block';
|
||||
arrow.textContent = '▼';
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
arrow.textContent = '▶';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleKeep(ruleId, keep) {
|
||||
vscode.postMessage({ type: 'toggleRule', ruleId, keep });
|
||||
const card = document.querySelector('.rule-card[data-ruleid="' + ruleId + '"]');
|
||||
if (!card) return;
|
||||
const btns = card.querySelectorAll('.toggle-btn');
|
||||
btns.forEach(b => b.classList.toggle('active', (keep && b.dataset.action === 'keep') || (!keep && b.dataset.action === 'comment')));
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
function updateRule(ruleId, field, value) {
|
||||
if (!editedRules[ruleId]) {
|
||||
editedRules[ruleId] = {};
|
||||
}
|
||||
editedRules[ruleId][field] = value;
|
||||
updateEditHint();
|
||||
}
|
||||
|
||||
function collectEditedRules() {
|
||||
const result = [];
|
||||
document.querySelectorAll('.rule-card').forEach(card => {
|
||||
const originalId = card.dataset.ruleid;
|
||||
const idInput = card.querySelector('.id-display-input');
|
||||
const ruleId = idInput ? idInput.value.trim() || originalId : originalId;
|
||||
const severityEl = card.querySelector('.edit-field select');
|
||||
const textareas = card.querySelectorAll('.edit-field textarea');
|
||||
const descEl = textareas[0];
|
||||
const msgEl = textareas[1];
|
||||
const langList = card.querySelector('.tag-list[data-field="languages"]');
|
||||
const exclList = card.querySelector('.tag-list[data-field="excludeLanguages"]');
|
||||
|
||||
const rule = {
|
||||
id: ruleId,
|
||||
severity: severityEl ? severityEl.value : 'warning',
|
||||
description: descEl ? descEl.value : '',
|
||||
message: msgEl ? msgEl.value : '',
|
||||
languages: langList ? Array.from(langList.querySelectorAll('.tag')).map(t => t.dataset.value) : [],
|
||||
excludeLanguages: exclList ? Array.from(exclList.querySelectorAll('.tag')).map(t => t.dataset.value) : [],
|
||||
};
|
||||
|
||||
result.push(rule);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function validate() {
|
||||
const rules = collectEditedRules();
|
||||
for (const rule of rules) {
|
||||
if (!rule.id || !rule.id.trim()) {
|
||||
return VALIDATION_ID_EMPTY;
|
||||
}
|
||||
if (!rule.description || !rule.description.trim()) {
|
||||
return VALIDATION_DESC_EMPTY.replace('{0}', rule.id);
|
||||
}
|
||||
if (!rule.message || !rule.message.trim()) {
|
||||
return VALIDATION_MSG_EMPTY.replace('{0}', rule.id);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function doConfirm() {
|
||||
const err = validate();
|
||||
if (err) {
|
||||
const errEl = document.getElementById('validationError');
|
||||
errEl.textContent = err;
|
||||
errEl.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
const edited = collectEditedRules();
|
||||
const hasEdits = Object.keys(editedRules).length > 0;
|
||||
vscode.postMessage({ type: 'confirm', editedRules: hasEdits ? edited : undefined });
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
vscode.postMessage({ type: 'cancel' });
|
||||
}
|
||||
|
||||
function updateSummary() {
|
||||
let keepCount = 0, commentCount = 0;
|
||||
document.querySelectorAll('.rule-card').forEach(card => {
|
||||
const ruleId = card.dataset.ruleid;
|
||||
const keepBtns = card.querySelectorAll('.toggle-btn');
|
||||
let isKept = true;
|
||||
keepBtns.forEach(b => {
|
||||
if (b.classList.contains('active') && b.dataset.action === 'comment') isKept = false;
|
||||
});
|
||||
if (isKept) keepCount++; else commentCount++;
|
||||
});
|
||||
document.getElementById('keepCount').textContent = keepCount;
|
||||
document.getElementById('commentCount').textContent = commentCount;
|
||||
}
|
||||
|
||||
function updateEditHint() {
|
||||
const count = Object.keys(editedRules).length;
|
||||
const hint = document.getElementById('editHint');
|
||||
const countEl = document.getElementById('editCount');
|
||||
if (count > 0) {
|
||||
hint.style.display = 'inline';
|
||||
countEl.textContent = count;
|
||||
} else {
|
||||
hint.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.target.classList.contains('tag-input') && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const input = e.target;
|
||||
const val = input.value.trim();
|
||||
if (!val) return;
|
||||
const ruleId = input.dataset.ruleid;
|
||||
const field = input.dataset.field;
|
||||
const list = input.parentElement.querySelector('.tag-list');
|
||||
|
||||
const existing = list.querySelectorAll('.tag');
|
||||
const exists = Array.from(existing).some(t => t.dataset.value === val);
|
||||
if (exists) { input.value = ''; return; }
|
||||
|
||||
const tag = document.createElement('span');
|
||||
tag.className = 'tag';
|
||||
tag.dataset.value = val;
|
||||
tag.innerHTML = val + '<span class="tag-remove">×</span>';
|
||||
list.appendChild(tag);
|
||||
input.value = '';
|
||||
|
||||
const tags = Array.from(list.querySelectorAll('.tag')).map(t => t.dataset.value);
|
||||
if (!editedRules[ruleId]) editedRules[ruleId] = {};
|
||||
editedRules[ruleId][field] = tags;
|
||||
updateEditHint();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('tag-remove')) {
|
||||
const tag = e.target.closest('.tag');
|
||||
const list = tag.closest('.tag-list');
|
||||
const ruleId = list.dataset.ruleid;
|
||||
const field = list.dataset.field;
|
||||
tag.remove();
|
||||
const remaining = Array.from(list.querySelectorAll('.tag')).map(t => t.dataset.value);
|
||||
if (!editedRules[ruleId]) editedRules[ruleId] = {};
|
||||
editedRules[ruleId][field] = remaining;
|
||||
updateEditHint();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
Reference in New Issue
Block a user