gitlink-cli/skills/gitlink-shared/scripts/md-to-html.js

329 lines
10 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
/**
* Markdown 报告 → HTML 可视化转换器
* 用法: node md-to-html.js <输入.md> [输出.html]
* 如果输出路径省略,自动在同目录生成同名 .html 文件
*/
const fs = require('fs');
const path = require('path');
const inputFile = process.argv[2];
if (!inputFile) {
console.error('用法: node md-to-html.js <输入.md> [输出.html]');
process.exit(1);
}
const outputFile = process.argv[3] || inputFile.replace(/\.md$/i, '.html');
const md = fs.readFileSync(inputFile, 'utf8');
// ==== 简易 Markdown → HTML 转换 ====
let html = md;
// 提取标题
const titleMatch = html.match(/^#\s+(.+)/m);
const pageTitle = titleMatch ? titleMatch[1].replace(/[*🔬📊📈]/g, '').trim() : path.basename(inputFile, '.md');
// 提取综合评分
const scoreMatch = html.match(/\*\*综合评分[:]\*\*\s*([\d.]+)\/10\s*(.*?)(?:\n|$)/);
const totalScore = scoreMatch ? scoreMatch[1] : null;
const scoreLevel = scoreMatch ? scoreMatch[2].trim() : '';
// 表格处理
function renderTable(tableHtml) {
const lines = tableHtml.split('\n').filter(l => l.trim());
if (lines.length < 2) return tableHtml;
// 提取表头
const header = lines[0].replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim());
const hasSeparator = lines[1] && lines[1].includes('---');
let result = '<div class="table-wrap"><table><thead><tr>';
result += header.map(h => `<th>${h}</th>`).join('');
result += '</tr></thead><tbody>';
const dataLines = hasSeparator ? lines.slice(2) : lines.slice(1);
dataLines.forEach(line => {
if (!line.trim()) return;
const cols = line.replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim());
result += '<tr>' + cols.map(c => {
// 处理 emoji 状态标记
let cellClass = '';
if (c.includes('✅')) cellClass = ' class="cell-ok"';
else if (c.includes('❌')) cellClass = ' class="cell-fail"';
else if (c.includes('🟢')) cellClass = ' class="cell-green"';
else if (c.includes('🟡')) cellClass = ' class="cell-yellow"';
else if (c.includes('🔴')) cellClass = ' class="cell-red"';
else if (c.includes('⚠️')) cellClass = ' class="cell-warn"';
return `<td${cellClass}>${c}</td>`;
}).join('') + '</tr>';
});
result += '</tbody></table></div>';
return result;
}
// 处理代码块
html = html.replace(/```[\s\S]*?```/g, m => {
const code = m.replace(/```\w*\n?/, '').replace(/```$/, '');
return `<pre class="code-block"><code>${escapeHtml(code)}</code></pre>`;
});
// 处理表格
html = html.replace(/\|(.+)\|\n\|[-| :]+\|\n(?:\|.+\|\n?)*/g, renderTable);
// 处理标题
html = html.replace(/^####\s+(.+)/gm, '<h4>$1</h4>');
html = html.replace(/^###\s+(.+)/gm, '<h3>$1</h3>');
html = html.replace(/^##\s+(.+)/gm, '<h2>$1</h2>');
html = html.replace(/^#\s+(.+)/gm, '<h1>$1</h1>');
// 处理粗体和行内代码
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
// 处理列表
html = html.replace(/^(\d+)\.\s+(.+)/gm, '<li data-index="$1">$2</li>');
html = html.replace(/^-\s+(.+)/gm, '<li>$1</li>');
html = html.replace(/(<li.*<\/li>\n)+/g, '<ol>$&</ol>');
// 处理段落
html = html.replace(/^(?!<[hHlLtTpPcCdiI]|<div|<pre|<table|<o).+$/gm, m => {
if (m.trim()) return `<p>${m.trim()}</p>`;
return m;
});
// 清理多余空行
html = html.replace(/\n{3,}/g, '\n\n');
function escapeHtml(text) {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 构建评分环
const scoreRing = totalScore ? `
<div class="score-section">
<div class="score-ring">
<svg viewBox="0 0 120 120" width="140" height="140">
<circle cx="60" cy="60" r="52" fill="none" stroke="#e2e8f0" stroke-width="8"/>
<circle cx="60" cy="60" r="52" fill="none" stroke="${getScoreColor(totalScore)}" stroke-width="8"
stroke-dasharray="${2 * Math.PI * 52}" stroke-dashoffset="${2 * Math.PI * 52 * (1 - totalScore / 10)}"
transform="rotate(-90, 60, 60)" stroke-linecap="round"
style="transition: stroke-dashoffset 1.5s ease-in-out;"/>
<text x="60" y="50" text-anchor="middle" font-size="32" font-weight="700" fill="#1e293b">${totalScore}</text>
<text x="60" y="72" text-anchor="middle" font-size="13" fill="#64748b">/ 10</text>
</svg>
</div>
<div class="score-label">${scoreLevel}</div>
</div>` : '';
function getScoreColor(score) {
const s = parseFloat(score);
if (s >= 8) return '#22c55e';
if (s >= 6) return '#3b82f6';
if (s >= 4) return '#f59e0b';
return '#ef4444';
}
// 构建完整 HTML
const fullHtml = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${pageTitle}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
background: #f1f5f9;
color: #1e293b;
line-height: 1.7;
}
.page {
max-width: 900px;
margin: 0 auto;
padding: 0 20px 60px;
}
.header-card {
background: linear-gradient(135deg, #1e293b, #334155);
color: white;
border-radius: 16px;
padding: 32px 40px;
margin: 24px 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
}
.header-card h1 {
font-size: 22px;
font-weight: 600;
line-height: 1.3;
}
.header-card .meta {
font-size: 13px;
color: #94a3b8;
margin-top: 6px;
}
.card {
background: white;
border-radius: 12px;
padding: 24px 28px;
margin-bottom: 16px;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
border: 1px solid #e2e8f0;
}
.card h2 {
font-size: 16px;
font-weight: 600;
color: #0f172a;
margin-bottom: 14px;
padding-bottom: 8px;
border-bottom: 2px solid #f1f5f9;
display: flex;
align-items: center;
gap: 8px;
}
.card h3 {
font-size: 14px;
font-weight: 600;
color: #334155;
margin: 16px 0 8px;
}
.card h4 {
font-size: 13px;
font-weight: 600;
color: #475569;
margin: 12px 0 6px;
}
.card p {
font-size: 14px;
color: #475569;
margin-bottom: 8px;
}
.card p:last-child { margin-bottom: 0; }
.card code {
background: #f1f5f9;
padding: 1px 6px;
border-radius: 4px;
font-size: 13px;
font-family: 'Cascadia Code', 'Fira Code', monospace;
color: #0f172a;
}
.card strong {
color: #0f172a;
}
.score-section {
text-align: center;
padding: 8px 0;
}
.score-ring {
display: inline-block;
}
.score-label {
font-size: 18px;
font-weight: 600;
margin-top: 4px;
}
.table-wrap {
overflow-x: auto;
margin: 10px 0;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th {
background: #f8fafc;
font-weight: 600;
text-align: left;
padding: 10px 12px;
border-bottom: 2px solid #e2e8f0;
color: #475569;
white-space: nowrap;
}
td {
padding: 9px 12px;
border-bottom: 1px solid #f1f5f9;
color: #334155;
}
tr:last-child td { border-bottom: none; }
tr:hover td { background: #f8fafc; }
.cell-ok { color: #16a34a; font-weight: 500; }
.cell-fail { color: #dc2626; }
.cell-green { color: #16a34a; }
.cell-yellow { color: #ca8a04; }
.cell-red { color: #dc2626; }
.cell-warn { color: #ea580c; }
ol {
padding-left: 20px;
margin: 8px 0;
}
ol li {
font-size: 14px;
color: #475569;
padding: 4px 0;
list-style-position: outside;
}
li::marker {
font-weight: 600;
color: #3b82f6;
}
.code-block {
background: #0f172a;
color: #e2e8f0;
padding: 16px 20px;
border-radius: 8px;
font-size: 13px;
overflow-x: auto;
margin: 12px 0;
line-height: 1.5;
font-family: 'Cascadia Code', 'Fira Code', monospace;
}
.footer {
text-align: center;
padding: 24px;
color: #94a3b8;
font-size: 12px;
}
@media (max-width: 640px) {
.page { padding: 0 12px 40px; }
.header-card { padding: 20px; flex-direction: column; text-align: center; }
.header-card h1 { font-size: 18px; }
.card { padding: 16px; }
}
@media print {
body { background: white; }
.header-card { break-inside: avoid; }
.card { break-inside: avoid; box-shadow: none; border: 1px solid #e2e8f0; }
}
</style>
</head>
<body>
<div class="page">
<div class="header-card">
<div>
<h1>${pageTitle}</h1>
<div class="meta">生成时间:${new Date().toLocaleString('zh-CN')}</div>
</div>
${scoreRing}
</div>
<div class="card report-content">
${html.split('\n').filter(l => {
// 去掉已经被处理过的原始 markdown 元素
return !l.match(/^\*\*数据来源/) && !l.match(/^\*\*注意/) && !l.match(/^\*自动生/);
}).join('\n')}
</div>
<div class="footer">
Powered by gitlink-cli · 数据来源于 GitLink 平台
</div>
</div>
</body>
</html>`;
fs.writeFileSync(outputFile, fullHtml, 'utf8');
console.log('✅ HTML 报告已生成: ' + outputFile);