wa-dev-tools/public/converter.html

214 lines
11 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>
<link href="/vendor/tailwind.min.css" rel="stylesheet">
<script>window.WA_TOOL_ID = 'converter';</script>
<script src="/shared.js?v=3"></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>
<!-- Yandex.Metrika counter -->
<script type="text/javascript">
(function(m,e,t,r,i,k,a){
m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)
})(window, document,'script','https://mc.yandex.ru/metrika/tag.js?id=108185982', 'ym');
ym(108185982, 'init', {ssr:true, webvisor:true, clickmap:true, ecommerce:"dataLayer", referrer: document.referrer, url: location.href, accurateTrackBounce:true, trackLinks:true});
</script>
<noscript><div><img src="https://mc.yandex.ru/watch/108185982" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter -->
</body>
</html>