- Remove duplicate badges on md, parser, placeholder pages - Custom logout modal (blur overlay, confirm/cancel buttons) - Replace native confirm() with styled modal - Cache bust shared.js?v=3
256 lines
13 KiB
HTML
256 lines
13 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>Markdown Viewer — WA Dev Tools</title>
|
||
<script src="/vendor/tailwind.js"></script>
|
||
<script>window.WA_TOOL_ID = 'md';</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>
|
||
<script src="/vendor/highlight.min.js"></script>
|
||
<link id="hljsTheme" href="/vendor/hljs-github-dark.css" rel="stylesheet">
|
||
<style>
|
||
/* Dropzone */
|
||
.md-dropzone { border: 2px dashed var(--surface-600); transition: all .3s; }
|
||
.md-dropzone.active { border-color: #0054e6; background: rgba(0,84,230,.05); }
|
||
|
||
/* Markdown rendered output */
|
||
.md-output {
|
||
color: var(--text-primary);
|
||
line-height: 1.75;
|
||
font-size: 15px;
|
||
}
|
||
.md-output h1 { font-size: 2em; font-weight: 800; margin: 1.2em 0 .6em; padding-bottom: .3em; border-bottom: 1px solid var(--surface-600); }
|
||
.md-output h2 { font-size: 1.5em; font-weight: 700; margin: 1em 0 .5em; padding-bottom: .25em; border-bottom: 1px solid var(--surface-600); }
|
||
.md-output h3 { font-size: 1.25em; font-weight: 600; margin: .9em 0 .4em; }
|
||
.md-output h4 { font-size: 1.1em; font-weight: 600; margin: .8em 0 .3em; }
|
||
.md-output p { margin: .6em 0; }
|
||
.md-output ul, .md-output ol { margin: .6em 0; padding-left: 1.8em; }
|
||
.md-output li { margin: .25em 0; }
|
||
.md-output ul li { list-style: disc; }
|
||
.md-output ol li { list-style: decimal; }
|
||
.md-output a { color: #0054e6; text-decoration: underline; text-underline-offset: 2px; }
|
||
.md-output blockquote { border-left: 3px solid #0054e6; padding: .5em 1em; margin: .8em 0; color: var(--text-secondary); background: rgba(0,84,230,.03); border-radius: 0 8px 8px 0; }
|
||
.md-output code { font-family: 'JetBrains Mono', monospace; font-size: .88em; padding: .15em .4em; border-radius: 4px; background: var(--surface-700); }
|
||
.md-output pre { margin: .8em 0; border-radius: 10px; overflow-x: auto; }
|
||
.md-output pre code { display: block; padding: 1em 1.2em; background: var(--surface-700); font-size: .85em; line-height: 1.6; border-radius: 10px; }
|
||
.md-output table { border-collapse: collapse; width: 100%; margin: .8em 0; font-size: .92em; }
|
||
.md-output th, .md-output td { border: 1px solid var(--surface-600); padding: .5em .8em; text-align: left; }
|
||
.md-output th { background: var(--surface-700); font-weight: 600; }
|
||
.md-output hr { border: none; border-top: 1px solid var(--surface-600); margin: 1.5em 0; }
|
||
.md-output img { max-width: 100%; border-radius: 8px; margin: .8em 0; }
|
||
|
||
/* Tabs */
|
||
.tab-btn { transition: all .2s; }
|
||
.tab-btn.active { color: #0054e6; border-color: #0054e6; }
|
||
</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-4xl mx-auto">
|
||
|
||
<!-- Header -->
|
||
<div class="page-header" style="text-align:center;">
|
||
<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>
|
||
MD · GitHub Flavored
|
||
</div>
|
||
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||
Просмотр <span class="text-accent">Markdown</span>
|
||
</h1>
|
||
<p class="description mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">Рендеринг Markdown с подсветкой синтаксиса и экспортом</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Input 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">
|
||
|
||
<!-- Tabs -->
|
||
<div class="flex gap-2 mb-5 border-b dark:border-surface-600 border-gray-200 pb-3">
|
||
<button class="tab-btn active text-sm font-medium px-3 py-1.5 border-b-2 border-transparent dark:text-gray-400 text-gray-500" data-tab="file">
|
||
Файл
|
||
</button>
|
||
<button class="tab-btn text-sm font-medium px-3 py-1.5 border-b-2 border-transparent dark:text-gray-400 text-gray-500" data-tab="text">
|
||
Текст
|
||
</button>
|
||
</div>
|
||
|
||
<!-- File tab -->
|
||
<div id="tabFile" class="tab-content">
|
||
<div id="dropzone" class="md-dropzone rounded-xl p-8 sm:p-10 text-center cursor-pointer group">
|
||
<svg class="mx-auto w-10 h-10 dark:text-gray-500 text-gray-400 group-hover:text-accent transition-colors mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||
</svg>
|
||
<p class="text-sm dark:text-gray-400 text-gray-500">
|
||
Перетащите <span class="text-accent font-medium">.md</span> файл сюда или <span class="text-accent font-medium cursor-pointer">выберите</span>
|
||
</p>
|
||
<p id="fileName" class="mt-2 text-xs font-mono dark:text-gray-600 text-gray-400 hidden"></p>
|
||
<input type="file" accept=".md,.markdown,.txt" id="fileInput" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" style="position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Text tab -->
|
||
<div id="tabText" class="tab-content hidden">
|
||
<textarea id="mdInput" class="w-full h-48 px-4 py-3 rounded-xl text-sm font-mono 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 resize-y transition-colors" placeholder="Вставьте Markdown текст..."></textarea>
|
||
<button id="renderBtn" class="mt-3 px-5 py-2.5 rounded-xl text-sm font-semibold bg-accent text-white hover:bg-accent-bright transition-all active:scale-[0.98]">
|
||
Рендер
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Output -->
|
||
<div id="outputCard" class="hidden mt-5 fade-up">
|
||
<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 shadow-sm dark:shadow-none">
|
||
<!-- Toolbar -->
|
||
<div class="flex items-center justify-between mb-4 pb-3 border-b dark:border-surface-600 border-gray-200">
|
||
<span class="text-xs font-mono dark:text-gray-500 text-gray-400 uppercase tracking-wider">Результат</span>
|
||
<div class="flex gap-2">
|
||
<button id="downloadHtmlBtn" class="px-3 py-1.5 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">
|
||
Скачать HTML
|
||
</button>
|
||
<button id="copyHtmlBtn" class="px-3 py-1.5 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-300 text-gray-600 hover:text-accent transition-colors border dark:border-surface-600 border-gray-200">
|
||
Скопировать HTML
|
||
</button>
|
||
<button id="clearBtn" class="px-3 py-1.5 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-300 text-gray-600 hover:text-danger transition-colors border dark:border-surface-600 border-gray-200">
|
||
Очистить
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<!-- Rendered markdown -->
|
||
<div id="mdOutput" class="md-output"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div><!-- /main-content -->
|
||
|
||
<script>
|
||
function applyHljsTheme() {
|
||
const isDark = document.documentElement.classList.contains('dark');
|
||
document.getElementById('hljsTheme').href = isDark ? '/vendor/hljs-github-dark.css' : '/vendor/hljs-github.css';
|
||
}
|
||
applyHljsTheme();
|
||
|
||
// Listen for theme changes from shared.js
|
||
document.addEventListener('themeChanged', applyHljsTheme);
|
||
// Also observe class changes on html element for theme toggle
|
||
const observer = new MutationObserver(applyHljsTheme);
|
||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||
|
||
// Tabs
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
const tab = btn.dataset.tab;
|
||
document.getElementById('tabFile').classList.toggle('hidden', tab !== 'file');
|
||
document.getElementById('tabText').classList.toggle('hidden', tab !== 'text');
|
||
});
|
||
});
|
||
|
||
// Marked config
|
||
marked.setOptions({
|
||
highlight: function(code, lang) {
|
||
if (lang && hljs.getLanguage(lang)) {
|
||
return hljs.highlight(code, { language: lang }).value;
|
||
}
|
||
return hljs.highlightAuto(code).value;
|
||
},
|
||
breaks: true,
|
||
gfm: true,
|
||
});
|
||
|
||
function renderMarkdown(md) {
|
||
const output = document.getElementById('mdOutput');
|
||
const card = document.getElementById('outputCard');
|
||
output.innerHTML = marked.parse(md);
|
||
card.classList.remove('hidden');
|
||
// Re-highlight any code blocks
|
||
output.querySelectorAll('pre code').forEach(block => {
|
||
hljs.highlightElement(block);
|
||
});
|
||
}
|
||
|
||
// File drop
|
||
const dropzone = document.getElementById('dropzone');
|
||
const fileInput = document.getElementById('fileInput');
|
||
|
||
// Make dropzone relative for absolute file input
|
||
dropzone.style.position = 'relative';
|
||
|
||
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('active'); });
|
||
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('active'));
|
||
dropzone.addEventListener('drop', e => {
|
||
e.preventDefault();
|
||
dropzone.classList.remove('active');
|
||
const file = e.dataTransfer.files[0];
|
||
if (file) readFile(file);
|
||
});
|
||
fileInput.addEventListener('change', e => {
|
||
if (e.target.files[0]) readFile(e.target.files[0]);
|
||
});
|
||
|
||
function readFile(file) {
|
||
const nameEl = document.getElementById('fileName');
|
||
nameEl.textContent = file.name;
|
||
nameEl.classList.remove('hidden');
|
||
const reader = new FileReader();
|
||
reader.onload = e => renderMarkdown(e.target.result);
|
||
reader.readAsText(file);
|
||
}
|
||
|
||
// Text render
|
||
document.getElementById('renderBtn').addEventListener('click', () => {
|
||
const text = document.getElementById('mdInput').value.trim();
|
||
if (text) renderMarkdown(text);
|
||
});
|
||
|
||
// Also render on Ctrl+Enter in textarea
|
||
document.getElementById('mdInput').addEventListener('keydown', e => {
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||
e.preventDefault();
|
||
document.getElementById('renderBtn').click();
|
||
}
|
||
});
|
||
|
||
// Copy HTML
|
||
document.getElementById('copyHtmlBtn').addEventListener('click', () => {
|
||
const html = document.getElementById('mdOutput').innerHTML;
|
||
navigator.clipboard.writeText(html).then(() => {
|
||
const btn = document.getElementById('copyHtmlBtn');
|
||
const orig = btn.textContent;
|
||
btn.textContent = 'Скопировано!';
|
||
btn.classList.add('text-accent');
|
||
setTimeout(() => { btn.textContent = orig; btn.classList.remove('text-accent'); }, 1500);
|
||
});
|
||
});
|
||
|
||
// Download HTML
|
||
document.getElementById('downloadHtmlBtn').addEventListener('click', () => {
|
||
const body = document.getElementById('mdOutput').innerHTML;
|
||
const fullHtml = '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<title>Markdown</title>\n<style>\nbody{font-family:sans-serif;max-width:800px;margin:40px auto;padding:0 20px;line-height:1.7;color:#333}\npre{background:#f5f5f5;padding:1em;border-radius:6px;overflow-x:auto}\ncode{font-family:monospace;font-size:.9em;background:#f0f0f0;padding:2px 5px;border-radius:3px}\npre code{background:none;padding:0}\ntable{border-collapse:collapse;width:100%}\nth,td{border:1px solid #ddd;padding:8px;text-align:left}\nth{background:#f5f5f5}\nblockquote{border-left:3px solid #0054e6;padding:.5em 1em;margin:1em 0;color:#666;background:#f9f9f9}\nimg{max-width:100%}\n</style>\n</head>\n<body>\n' + body + '\n</body>\n</html>';
|
||
const blob = new Blob([fullHtml], { type: 'text/html' });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'markdown.html';
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
});
|
||
|
||
// Clear
|
||
document.getElementById('clearBtn').addEventListener('click', () => {
|
||
document.getElementById('mdOutput').innerHTML = '';
|
||
document.getElementById('outputCard').classList.add('hidden');
|
||
document.getElementById('mdInput').value = '';
|
||
document.getElementById('fileName').classList.add('hidden');
|
||
fileInput.value = '';
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|