214 lines
11 KiB
HTML
214 lines
11 KiB
HTML
<!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=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 ↔ 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 → 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 → 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 ' || '') + ')';
|
||
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>
|