wa-dev-tools/public/converter.html
treamz 89345551ef Unified headers: centered with badge tags on all 12 tool pages
- All pages: centered header with animated badge (format tags)
- Badge format: colored dot + tech tags (PNG·JPG·WebP etc)
- Updated descriptions to be more detailed
- Consistent style across compress, video, http, redirects, password,
  pdf, parser, md, sanitizer, converter, formatter, placeholder
2026-03-22 10:45:38 +03:00

202 lines
10 KiB
HTML
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.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Format Converter — WA Dev Tools</title>
<script src="/vendor/tailwind.js"></script>
<script>window.WA_TOOL_ID = 'converter';</script>
<script src="/shared.js?v=2"></script>
<link href="/vendor/fonts.css" rel="stylesheet">
<link href="/shared.css" rel="stylesheet">
<script src="/vendor/marked.min.js"></script>
<style>
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.fade-up{animation:fadeUp .4s ease-out forwards}.fade-up-delay{animation:fadeUp .4s ease-out .1s forwards;opacity:0}
.tool-btn{transition:all .2s}.tool-btn.active{color:#0054e6;background:rgba(0,84,230,.08);border-color:rgba(0,84,230,.3)}
textarea{font-family:'JetBrains Mono',monospace;font-size:13px;resize:vertical}
</style>
</head>
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
<div class="main-content">
<div class="tool-container relative z-10 max-w-3xl mx-auto px-4 py-8 sm:py-12">
<div class="page-header" style="text-align:center; mb-8 fade-up">
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
HTML &harr; Markdown
</div>
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
Format <span class="text-accent">Converter</span>
</h1>
<p class="description mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">Конвертация между HTML и Markdown с сохранением форматирования</p>
</div>
<!-- Tool Toggle -->
<div class="flex gap-2 mb-5 fade-up-delay">
<button class="tool-btn active flex-1 px-4 py-3 rounded-xl text-center border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 cursor-pointer" data-tool="htmlToMd">
<div class="text-sm font-semibold">HTML &rarr; Markdown</div>
</button>
<button class="tool-btn flex-1 px-4 py-3 rounded-xl text-center border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 cursor-pointer" data-tool="mdToHtml">
<div class="text-sm font-semibold">Markdown &rarr; HTML</div>
</button>
</div>
<!-- Processing Card -->
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
<div class="mb-4">
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Input</label>
<textarea id="inputArea" rows="10" placeholder="Вставьте HTML..."
class="w-full px-4 py-3 rounded-xl dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 dark:text-gray-200 text-gray-700 focus:outline-none focus:border-accent/50 transition-colors"></textarea>
</div>
<button id="processBtn"
class="w-full py-3 rounded-xl font-semibold text-sm tracking-wide transition-all duration-300
bg-accent text-white border border-accent cursor-pointer
hover:bg-accent-bright hover:shadow-lg hover:shadow-accent/20 active:scale-[0.98]">
Конвертировать
</button>
<div id="outputBlock" class="mt-5 hidden">
<div class="flex items-center justify-between mb-1.5">
<label class="text-xs font-medium dark:text-gray-500 text-gray-400 font-mono uppercase tracking-wider">Output</label>
<div class="flex items-center gap-2">
<span id="stats" class="text-[10px] font-mono dark:text-gray-600 text-gray-400"></span>
<button id="copyBtn" class="px-3 py-1 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">Копировать</button>
<button id="clearBtn" class="px-3 py-1 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-400 text-gray-500 hover:text-danger transition-colors border dark:border-surface-600 border-gray-200">Очистить</button>
</div>
</div>
<textarea id="outputArea" rows="12" readonly
class="w-full px-4 py-3 rounded-xl dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 dark:text-gray-200 text-gray-700 focus:outline-none transition-colors"></textarea>
</div>
</div>
</div>
</div>
<script>
let activeTool = 'htmlToMd';
const $ = id => document.getElementById(id);
document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tool-btn[data-tool]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeTool = btn.dataset.tool;
$('inputArea').placeholder = activeTool === 'htmlToMd' ? 'Вставьте HTML...' : 'Вставьте Markdown...';
});
});
$('processBtn').addEventListener('click', () => {
const input = $('inputArea').value;
if (!input.trim()) return;
let output = '';
try {
output = activeTool === 'htmlToMd' ? htmlToMarkdown(input) : mdToHtml(input);
} catch (e) { output = 'Error: ' + e.message; }
$('outputArea').value = output;
$('outputBlock').classList.remove('hidden');
const diff = output.length - input.length;
$('stats').textContent = `${input.length} \u2192 ${output.length} (${diff > 0 ? '+' : ''}${diff})`;
});
$('copyBtn').addEventListener('click', () => {
navigator.clipboard.writeText($('outputArea').value).then(() => {
const b = $('copyBtn'); const o = b.textContent;
b.textContent = '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u043e!'; setTimeout(() => { b.textContent = o; }, 1500);
});
});
$('clearBtn').addEventListener('click', () => {
$('inputArea').value = ''; $('outputArea').value = ''; $('outputBlock').classList.add('hidden');
});
// ==================== HTML → Markdown ====================
function htmlToMarkdown(html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
return nodesToMd(doc.body.childNodes).replace(/\n{3,}/g, '\n\n').trim();
}
function nodesToMd(nodes) {
let md = '';
for (const node of nodes) {
if (node.nodeType === 3) md += node.textContent.replace(/\s+/g, ' ');
else if (node.nodeType === 1) md += elemToMd(node);
}
return md;
}
function elemToMd(el) {
const tag = el.tagName.toLowerCase();
const inner = () => nodesToMd(el.childNodes);
switch (tag) {
case 'h1': return '\n\n# ' + inner().trim() + '\n\n';
case 'h2': return '\n\n## ' + inner().trim() + '\n\n';
case 'h3': return '\n\n### ' + inner().trim() + '\n\n';
case 'h4': return '\n\n#### ' + inner().trim() + '\n\n';
case 'h5': return '\n\n##### ' + inner().trim() + '\n\n';
case 'h6': return '\n\n###### ' + inner().trim() + '\n\n';
case 'p': return '\n\n' + inner().trim() + '\n\n';
case 'br': return '\n';
case 'hr': return '\n\n---\n\n';
case 'strong': case 'b': return '**' + inner().trim() + '**';
case 'em': case 'i': return '*' + inner().trim() + '*';
case 'del': case 's': return '~~' + inner().trim() + '~~';
case 'a': return '[' + inner().trim() + '](' + (el.getAttribute('href') || '') + ')';
case 'img': return '![' + (el.getAttribute('alt') || '') + '](' + (el.getAttribute('src') || '') + ')';
case 'code':
if (el.parentElement && el.parentElement.tagName.toLowerCase() === 'pre') return el.textContent;
return '`' + el.textContent + '`';
case 'pre': {
const code = el.querySelector('code');
const lang = code ? (code.className.match(/language-(\w+)/) || [])[1] || '' : '';
return '\n\n```' + lang + '\n' + (code ? code.textContent : el.textContent) + '\n```\n\n';
}
case 'blockquote': return '\n\n' + inner().trim().split('\n').map(l => '> ' + l).join('\n') + '\n\n';
case 'ul': {
let r = '\n';
for (const c of el.children) {
if (c.tagName && c.tagName.toLowerCase() === 'li') r += '- ' + nodesToMd(c.childNodes).trim() + '\n';
}
return r + '\n';
}
case 'ol': {
let r = '\n', n = 1;
for (const c of el.children) {
if (c.tagName && c.tagName.toLowerCase() === 'li') { r += n + '. ' + nodesToMd(c.childNodes).trim() + '\n'; n++; }
}
return r + '\n';
}
case 'table': return '\n\n' + tableToMd(el) + '\n\n';
case 'script': case 'style': return '';
default: return inner();
}
}
function tableToMd(table) {
const rows = [];
table.querySelectorAll('tr').forEach(tr => {
const cells = [];
tr.querySelectorAll('th, td').forEach(c => cells.push(c.textContent.trim()));
if (cells.length) rows.push(cells);
});
if (!rows.length) return '';
const cols = Math.max(...rows.map(r => r.length));
rows.forEach(r => { while (r.length < cols) r.push(''); });
const w = Array(cols).fill(3);
for (let c = 0; c < cols; c++) for (const r of rows) w[c] = Math.max(w[c], r[c].length);
let md = '| ' + rows[0].map((c, i) => c.padEnd(w[i])).join(' | ') + ' |\n';
md += '| ' + w.map(v => '-'.repeat(v)).join(' | ') + ' |\n';
for (let r = 1; r < rows.length; r++) md += '| ' + rows[r].map((c, i) => c.padEnd(w[i])).join(' | ') + ' |\n';
return md.trimEnd();
}
// ==================== Markdown → HTML ====================
function mdToHtml(md) {
return marked.parse(md);
}
</script>
</body>
</html>