wa-dev-tools/public/parser.html
2026-03-21 22:06:15 +03:00

565 lines
34 KiB
HTML

<!DOCTYPE html>
<html lang="ru" class="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>URL Parser — images.wadevelop.ru</title>
<script src="/vendor/tailwind.js"></script>
<link href="/vendor/fonts.css" rel="stylesheet">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
mono: ['JetBrains Mono', 'monospace'],
sans: ['Manrope', 'sans-serif'],
},
colors: {
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
warn: '#ffd600',
danger: '#ff5252',
}
}
}
}
</script>
<style>
/* Sidebar */
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
.main-content{margin-left:56px}
@media(max-width:640px){
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
.sidebar-logo{display:none}
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
.main-content{margin-left:0;padding-bottom:60px}
}
:root {
--bg: #f5f5f7;
--bg-grad1: rgba(0, 67, 184, 0.04);
--bg-grad2: rgba(0, 50, 140, 0.03);
--surface-800: rgba(255,255,255,0.85);
--surface-700: #f0f0f3;
--surface-600: #dde0e6;
--text-primary: #1a1a2e;
--text-secondary: #555;
--text-muted: #888;
--select-bg: #f0f0f3;
--select-color: #1a1a2e;
}
.dark {
--bg: #060c18;
--bg-grad1: rgba(0, 84, 230, 0.03);
--bg-grad2: rgba(0, 67, 184, 0.02);
--surface-800: rgba(9,16,32,0.8);
--surface-700: #0d1828;
--surface-600: #162040;
--text-primary: #fff;
--text-secondary: #c0c0c0;
--text-muted: #666;
--select-bg: #091020;
--select-color: #e0e0e0;
}
body {
background: var(--bg);
background-image:
radial-gradient(ellipse at 20% 50%, var(--bg-grad1) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, var(--bg-grad2) 0%, transparent 50%);
min-height: 100vh;
transition: background 0.3s;
}
.dark .noise::before {
content: '';
position: fixed;
inset: 0;
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 0;
}
.theme-toggle { position: relative; width: 44px; height: 24px; border-radius: 12px; cursor: pointer; transition: background 0.3s; }
.dark .theme-toggle { background: #162040; }
.theme-toggle { background: #dde0e6; }
.theme-toggle-knob { position: absolute; top: 2px; width: 20px; height: 20px; border-radius: 50%; transition: left 0.3s, background 0.3s; display: flex; align-items: center; justify-content: center; }
.dark .theme-toggle-knob { left: 22px; background: #0d1828; }
.theme-toggle-knob { left: 2px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
.fade-up { animation: fadeUp 0.4s ease-out forwards; }
.fade-up-delay { animation: fadeUp 0.4s ease-out 0.1s forwards; opacity: 0; }
@keyframes pulse-border { 0%, 100% { border-color: rgba(0, 84, 230, 0.2); } 50% { border-color: rgba(0, 84, 230, 0.5); } }
.processing { animation: pulse-border 1.5s ease-in-out infinite; }
.article-content { color: var(--text-primary); line-height: 1.75; font-size: 15px; }
.article-content h1 { font-size: 1.8em; font-weight: 800; margin: 1em 0 .5em; }
.article-content h2 { font-size: 1.4em; font-weight: 700; margin: .9em 0 .4em; }
.article-content h3 { font-size: 1.2em; font-weight: 600; margin: .8em 0 .3em; }
.article-content p { margin: .6em 0; }
.article-content ul, .article-content ol { margin: .6em 0; padding-left: 1.8em; }
.article-content li { margin: .25em 0; }
.article-content ul li { list-style: disc; }
.article-content ol li { list-style: decimal; }
.article-content a { color: #0054e6; text-decoration: underline; text-underline-offset: 2px; }
.article-content 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; }
.article-content code { font-family: 'JetBrains Mono', monospace; font-size: .88em; padding: .15em .4em; border-radius: 4px; background: var(--surface-700); }
.article-content pre { margin: .8em 0; border-radius: 10px; overflow-x: auto; }
.article-content pre code { display: block; padding: 1em 1.2em; background: var(--surface-700); }
.article-content img { max-width: 100%; border-radius: 8px; margin: .8em 0; }
.article-content table { border-collapse: collapse; width: 100%; margin: .8em 0; }
.article-content th, .article-content td { border: 1px solid var(--surface-600); padding: .5em .8em; text-align: left; }
.article-content th { background: var(--surface-700); font-weight: 600; }
.mode-btn { transition: all .2s; }
.mode-btn.active { color: #0054e6; background: rgba(0,84,230,.08); border-color: rgba(0,84,230,.3); }
</style>
</head>
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-logo">WA</div>
<div class="sidebar-nav">
<a href="/" class="sidebar-link" title="Главная">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
</a>
<a href="/compress" class="sidebar-link" title="Картинки">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
</a>
<a href="/md" class="sidebar-link" title="Markdown">
<svg width="22" height="22" 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.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.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>
</a>
<a href="/placeholder" class="sidebar-link" title="Placeholder">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
</a>
<a href="/parser" class="sidebar-link active" title="Parser">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
</a>
<a href="/password" class="sidebar-link" title="Password">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
</a>
<a href="/sanitizer" class="sidebar-link" title="Sanitizer">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
</a>
<a href="/converter" class="sidebar-link" title="Converter">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
</a>
<a href="/formatter" class="sidebar-link" title="Formatter">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
</a>
<a href="/editor" class="sidebar-link" title="Editor">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
</a>
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
</a>
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
</a>
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
</a>
</div>
<div class="sidebar-bottom">
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
<div class="theme-toggle-knob">
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
</div>
</div>
</div>
</nav>
<div class="main-content">
<div class="relative z-10 max-w-3xl mx-auto px-4 py-8 sm:py-12">
<!-- Header -->
<div class="text-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>
URL Article Parser
</div>
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
Парсер <span class="text-accent">статей</span>
</h1>
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">извлечение контента из любой веб-страницы</p>
</div>
<!-- Input Card -->
<div id="inputCard" 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="flex gap-2">
<input type="url" id="urlInput" placeholder="https://example.com/article"
class="flex-1 px-4 py-3 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-xl text-sm font-mono focus:outline-none focus:border-accent/50 transition-colors dark:text-white text-gray-800"
>
<button id="parseBtn"
class="px-6 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] whitespace-nowrap">
Парсить
</button>
</div>
<!-- Mode buttons -->
<div class="flex gap-2 mt-4">
<button class="mode-btn active flex-1 px-3 py-2 rounded-lg text-xs font-medium border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 transition-colors" data-mode="full">
Полный
</button>
<button class="mode-btn flex-1 px-3 py-2 rounded-lg text-xs font-medium border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 transition-colors" data-mode="metadata">
Метаданные
</button>
<button class="mode-btn flex-1 px-3 py-2 rounded-lg text-xs font-medium border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 transition-colors" data-mode="text">
Текст
</button>
<button class="mode-btn flex-1 px-3 py-2 rounded-lg text-xs font-medium border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 transition-colors" data-mode="preview">
Превью
</button>
</div>
</div>
<!-- Error -->
<div id="errorBlock" class="hidden mt-5 bg-danger/10 border border-danger/20 rounded-xl px-5 py-4 text-sm text-danger font-medium fade-up"></div>
<!-- Loading -->
<div id="loadingBlock" class="hidden mt-5">
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-8 backdrop-blur-sm shadow-sm dark:shadow-none text-center processing">
<div class="inline-flex items-center gap-3">
<svg class="animate-spin w-5 h-5 text-accent" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/></svg>
<span class="text-sm dark:text-gray-400 text-gray-500 font-mono">Загружаю и парсю страницу...</span>
</div>
</div>
</div>
<!-- Result: Full article -->
<div id="resultFull" class="hidden mt-5 fade-up space-y-4">
<!-- Preview card -->
<div id="previewCard" class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl overflow-hidden backdrop-blur-sm shadow-sm dark:shadow-none">
<img id="leadImage" class="w-full h-48 object-cover hidden" alt="">
<div class="p-5 sm:p-7">
<div class="flex items-center gap-2 mb-2">
<span id="siteName" class="text-xs font-mono dark:text-gray-500 text-gray-400"></span>
<span id="articleDate" class="text-xs font-mono dark:text-gray-500 text-gray-400"></span>
</div>
<h2 id="articleTitle" class="text-xl font-bold dark:text-white text-gray-900 mb-2"></h2>
<p id="articleAuthor" class="text-sm dark:text-gray-500 text-gray-400 hidden"></p>
<p id="articleExcerpt" class="text-sm dark:text-gray-400 text-gray-500 mt-2 leading-relaxed"></p>
<div class="flex items-center gap-3 mt-3">
<span id="wordCount" class="text-xs font-mono dark:text-gray-600 text-gray-400"></span>
<span id="articleLang" class="text-xs font-mono dark:text-gray-600 text-gray-400"></span>
<span id="imageCount" class="text-xs font-mono dark:text-gray-600 text-gray-400"></span>
</div>
</div>
</div>
<!-- Action buttons -->
<div class="flex gap-2 flex-wrap">
<button id="copyJsonBtn" class="px-4 py-2 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">
Скопировать JSON
</button>
<button id="copyTextBtn" class="px-4 py-2 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">
Скопировать текст
</button>
<button id="copyHtmlBtn" class="px-4 py-2 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="ml-auto px-4 py-2 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>
<!-- Article content -->
<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">
<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="viewHtmlBtn" class="mode-btn active px-3 py-1 rounded text-xs font-medium dark:text-gray-400 text-gray-500 border dark:border-surface-600 border-gray-200" data-view="html">HTML</button>
<button id="viewTextBtn" class="mode-btn px-3 py-1 rounded text-xs font-medium dark:text-gray-400 text-gray-500 border dark:border-surface-600 border-gray-200" data-view="text">Text</button>
<button id="viewJsonBtn" class="mode-btn px-3 py-1 rounded text-xs font-medium dark:text-gray-400 text-gray-500 border dark:border-surface-600 border-gray-200" data-view="json">JSON</button>
</div>
</div>
<div id="contentHtml" class="article-content"></div>
<pre id="contentText" class="hidden whitespace-pre-wrap text-sm font-mono dark:text-gray-300 text-gray-600 leading-relaxed max-h-[600px] overflow-y-auto"></pre>
<pre id="contentJson" class="hidden whitespace-pre-wrap text-xs font-mono dark:text-gray-300 text-gray-600 leading-relaxed max-h-[600px] overflow-y-auto dark:bg-surface-700 bg-gray-50 p-4 rounded-xl"></pre>
</div>
</div>
<!-- Result: Metadata only -->
<div id="resultMetadata" 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">
<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>
<button id="copyMetaJsonBtn" 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">
Скопировать
</button>
</div>
<pre id="metadataJson" class="whitespace-pre-wrap text-xs font-mono dark:text-gray-300 text-gray-600 leading-relaxed dark:bg-surface-700 bg-gray-50 p-4 rounded-xl"></pre>
</div>
</div>
<!-- Result: Text only -->
<div id="resultText" 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">
<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>
<button id="copyPlainTextBtn" 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">
Скопировать
</button>
</div>
<pre id="plainText" class="whitespace-pre-wrap text-sm dark:text-gray-300 text-gray-600 leading-relaxed max-h-[600px] overflow-y-auto"></pre>
</div>
</div>
<!-- Result: Preview -->
<div id="resultPreview" class="hidden mt-5 fade-up">
<div id="previewLinkCard" class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl overflow-hidden backdrop-blur-sm shadow-sm dark:shadow-none">
<img id="previewImg" class="w-full h-52 object-cover hidden" alt="">
<div class="p-5 sm:p-7">
<p id="previewSite" class="text-xs font-mono dark:text-gray-500 text-gray-400 mb-1"></p>
<h3 id="previewTitle" class="text-lg font-bold dark:text-white text-gray-900 mb-2"></h3>
<p id="previewDesc" class="text-sm dark:text-gray-400 text-gray-500 leading-relaxed"></p>
</div>
</div>
<button id="copyPreviewJsonBtn" class="mt-3 px-4 py-2 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">
Скопировать JSON
</button>
</div>
<!-- API Reference -->
<div class="mt-8 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">
<h3 class="text-sm font-semibold dark:text-white text-gray-800 mb-3">API</h3>
<div class="space-y-2 text-xs font-mono dark:text-gray-400 text-gray-500 leading-relaxed">
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/parse?url=URL</code> — полный парсинг (title, content, images, links)</p>
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/metadata?url=URL</code> — только метаданные</p>
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/text?url=URL</code> — только plain text</p>
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/preview?url=URL</code> — превью-карточка (title + image + description)</p>
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/health</code> — проверка доступности</p>
</div>
</div>
</div>
</div>
</div><!-- /main-content -->
<script>
// Theme
const html = document.documentElement;
const savedTheme = localStorage.getItem('theme');
if (savedTheme) { html.className = savedTheme; }
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
document.getElementById('themeToggle').addEventListener('click', () => {
const isDark = html.classList.toggle('dark');
localStorage.setItem('theme', isDark ? 'dark' : '');
});
// State
let currentMode = 'full';
let lastResult = null;
let lastMetadata = null;
let lastText = null;
let lastPreview = null;
const $ = id => document.getElementById(id);
// Mode buttons
document.querySelectorAll('.mode-btn[data-mode]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.mode-btn[data-mode]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentMode = btn.dataset.mode;
});
});
// View toggle (HTML/Text/JSON)
document.querySelectorAll('.mode-btn[data-view]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.mode-btn[data-view]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const view = btn.dataset.view;
$('contentHtml').classList.toggle('hidden', view !== 'html');
$('contentText').classList.toggle('hidden', view !== 'text');
$('contentJson').classList.toggle('hidden', view !== 'json');
});
});
// Parse
$('parseBtn').addEventListener('click', doParse);
$('urlInput').addEventListener('keydown', e => { if (e.key === 'Enter') doParse(); });
async function doParse() {
const url = $('urlInput').value.trim();
if (!url) return;
hideAll();
$('loadingBlock').classList.remove('hidden');
try {
if (currentMode === 'full') {
const res = await fetch('/parse?url=' + encodeURIComponent(url));
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Ошибка');
lastResult = data;
showFullResult(data);
} else if (currentMode === 'metadata') {
const res = await fetch('/metadata?url=' + encodeURIComponent(url));
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Ошибка');
lastMetadata = data;
showMetadata(data);
} else if (currentMode === 'text') {
const res = await fetch('/text?url=' + encodeURIComponent(url));
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || 'Ошибка');
}
const text = await res.text();
lastText = text;
showText(text);
} else if (currentMode === 'preview') {
const res = await fetch('/preview?url=' + encodeURIComponent(url));
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Ошибка');
lastPreview = data;
showPreview(data);
}
} catch (err) {
$('errorBlock').textContent = err.message;
$('errorBlock').classList.remove('hidden');
} finally {
$('loadingBlock').classList.add('hidden');
}
}
function hideAll() {
$('errorBlock').classList.add('hidden');
$('resultFull').classList.add('hidden');
$('resultMetadata').classList.add('hidden');
$('resultText').classList.add('hidden');
$('resultPreview').classList.add('hidden');
}
function showFullResult(data) {
// Lead image
if (data.lead_image) {
$('leadImage').src = data.lead_image;
$('leadImage').classList.remove('hidden');
} else {
$('leadImage').classList.add('hidden');
}
$('articleTitle').textContent = data.title || 'Без заголовка';
$('siteName').textContent = data.site || '';
$('articleDate').textContent = data.date_published ? new Date(data.date_published).toLocaleDateString('ru') : '';
if (data.author) {
$('articleAuthor').textContent = data.author;
$('articleAuthor').classList.remove('hidden');
} else {
$('articleAuthor').classList.add('hidden');
}
$('articleExcerpt').textContent = data.excerpt || '';
$('wordCount').textContent = data.word_count ? data.word_count + ' слов' : '';
$('articleLang').textContent = data.lang ? 'lang: ' + data.lang : '';
$('imageCount').textContent = data.images ? data.images.length + ' изобр.' : '';
// Content views
$('contentHtml').innerHTML = data.content_html || '<p class="dark:text-gray-500 text-gray-400">Контент не извлечён</p>';
$('contentText').textContent = data.content_text || '';
$('contentJson').textContent = JSON.stringify(data, null, 2);
// Reset view to HTML
document.querySelectorAll('.mode-btn[data-view]').forEach(b => b.classList.remove('active'));
$('viewHtmlBtn').classList.add('active');
$('contentHtml').classList.remove('hidden');
$('contentText').classList.add('hidden');
$('contentJson').classList.add('hidden');
$('resultFull').classList.remove('hidden');
}
function showMetadata(data) {
$('metadataJson').textContent = JSON.stringify(data, null, 2);
$('resultMetadata').classList.remove('hidden');
}
function showText(text) {
$('plainText').textContent = text;
$('resultText').classList.remove('hidden');
}
function showPreview(data) {
if (data.image) {
$('previewImg').src = data.image;
$('previewImg').classList.remove('hidden');
} else {
$('previewImg').classList.add('hidden');
}
$('previewSite').textContent = data.site || '';
$('previewTitle').textContent = data.title || '';
$('previewDesc').textContent = data.description || '';
$('resultPreview').classList.remove('hidden');
}
// Copy helpers
function copyToClipboard(text, btn) {
navigator.clipboard.writeText(text).then(() => {
const orig = btn.textContent;
btn.textContent = 'Скопировано!';
btn.classList.add('text-accent');
setTimeout(() => { btn.textContent = orig; btn.classList.remove('text-accent'); }, 1500);
});
}
$('copyJsonBtn').addEventListener('click', () => {
if (lastResult) copyToClipboard(JSON.stringify(lastResult, null, 2), $('copyJsonBtn'));
});
$('copyTextBtn').addEventListener('click', () => {
if (lastResult) copyToClipboard(lastResult.content_text || '', $('copyTextBtn'));
});
$('copyHtmlBtn').addEventListener('click', () => {
if (lastResult) copyToClipboard(lastResult.content_html || '', $('copyHtmlBtn'));
});
$('copyMetaJsonBtn').addEventListener('click', () => {
if (lastMetadata) copyToClipboard(JSON.stringify(lastMetadata, null, 2), $('copyMetaJsonBtn'));
});
$('copyPlainTextBtn').addEventListener('click', () => {
if (lastText) copyToClipboard(lastText, $('copyPlainTextBtn'));
});
$('copyPreviewJsonBtn').addEventListener('click', () => {
if (lastPreview) copyToClipboard(JSON.stringify(lastPreview, null, 2), $('copyPreviewJsonBtn'));
});
// Clear
$('clearBtn').addEventListener('click', () => {
hideAll();
lastResult = null;
lastMetadata = null;
lastText = null;
lastPreview = null;
$('urlInput').value = '';
$('urlInput').focus();
});
// Auto-focus
$('urlInput').focus();
</script>
</body>
</html>