markdown在线预览编辑器 html

markdown在线预览编辑器 html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MarkFlow · Markdown Reader</title>

<!-- Marked.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.6/marked.min.js"></script>
<!-- highlight.js -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/tokyo-night-dark.min.css" id="hljs-theme">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- KaTeX -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/katex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script>
<!-- Mermaid -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.6.1/mermaid.min.js"></script>

<style>
  @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Source+Serif+4:ital,opsz,wght@0,8..60,300;0,8..60,400;0,8..60,600;1,8..60,300;1,8..60,400&family=JetBrains+Mono:wght@400;600&display=swap');

  :root {
    --bg: #0f0e17;
    --bg2: #1a1928;
    --bg3: #242338;
    --surface: #1e1d2e;
    --border: #2e2c45;
    --accent: #e8c547;
    --accent2: #ff6b6b;
    --accent3: #64d9b8;
    --text: #fffffe;
    --text2: #a7a5c0;
    --text3: #6b697f;
    --sidebar-w: 280px;
    --header-h: 54px;
    --radius: 10px;
    --font-body: 'Source Serif 4', Georgia, serif;
    --font-display: 'Playfair Display', Georgia, serif;
    --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
    --prose-width: 760px;
  }

  [data-theme="light"] {
    --bg: #faf9f5;
    --bg2: #f0ede4;
    --bg3: #e8e4d9;
    --surface: #ffffff;
    --border: #d8d4c8;
    --accent: #c9952a;
    --accent2: #d94f4f;
    --accent3: #2a8a72;
    --text: #1a1814;
    --text2: #5a5648;
    --text3: #8a8070;
  }

  [data-theme="sepia"] {
    --bg: #f4ede0;
    --bg2: #ece3d0;
    --bg3: #e0d5bf;
    --surface: #faf5eb;
    --border: #d4c9b0;
    --accent: #9b6a2f;
    --accent2: #b55a3a;
    --accent3: #3a7a5a;
    --text: #2a2018;
    --text2: #6a5840;
    --text3: #9a8870;
  }

  [data-theme="forest"] {
    --bg: #0d1a0f;
    --bg2: #152018;
    --bg3: #1e2d21;
    --surface: #192219;
    --border: #2a3d2c;
    --accent: #7ec850;
    --accent2: #f0a040;
    --accent3: #50c8c0;
    --text: #e8f5e0;
    --text2: #9ab890;
    --text3: #608060;
  }

  * { box-sizing: border-box; margin: 0; padding: 0; }

  html, body {
    height: 100%;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-body);
    transition: background 0.3s, color 0.3s;
  }

  /* ── Layout ── */
  .app {
    display: grid;
    grid-template-rows: var(--header-h) 1fr;
    grid-template-columns: var(--sidebar-w) 1fr;
    height: 100vh;
    overflow: hidden;
  }

  .app.no-sidebar {
    grid-template-columns: 0 1fr;
  }

  /* ── Header ── */
  header {
    grid-column: 1 / -1;
    background: var(--bg2);
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    padding: 0 16px;
    gap: 12px;
    z-index: 10;
    backdrop-filter: blur(10px);
  }

  .logo {
    font-family: var(--font-display);
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--accent);
    letter-spacing: -0.02em;
    white-space: nowrap;
  }

  .logo span {
    color: var(--text2);
    font-weight: 300;
    font-style: italic;
  }

  .header-sep { flex: 1; }

  .header-actions {
    display: flex;
    align-items: center;
    gap: 6px;
  }

  .btn {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 5px 12px;
    border-radius: 6px;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text2);
    font-size: 0.75rem;
    font-family: var(--font-mono);
    cursor: pointer;
    transition: all 0.18s;
    white-space: nowrap;
  }
  .btn:hover { background: var(--bg3); color: var(--text); border-color: var(--accent); }
  .btn.active { background: var(--accent); color: var(--bg); border-color: var(--accent); }
  .btn svg { width: 13px; height: 13px; flex-shrink: 0; }

  .btn-icon {
    width: 30px;
    height: 30px;
    padding: 0;
    justify-content: center;
    border-radius: 6px;
  }

  /* Theme selector */
  .theme-select {
    appearance: none;
    background: var(--bg3);
    border: 1px solid var(--border);
    color: var(--text2);
    font-family: var(--font-mono);
    font-size: 0.72rem;
    padding: 4px 8px;
    border-radius: 6px;
    cursor: pointer;
    outline: none;
  }
  .theme-select:hover { border-color: var(--accent); color: var(--text); }

  /* Font size */
  .font-size-group {
    display: flex;
    gap: 2px;
  }

  /* ── Sidebar ── */
  .sidebar {
    background: var(--bg2);
    border-right: 1px solid var(--border);
    overflow: hidden;
    display: flex;
    flex-direction: column;
    transition: width 0.25s cubic-bezier(0.4,0,0.2,1);
  }

  .no-sidebar .sidebar { display: none; }

  .sidebar-header {
    padding: 14px 16px 10px;
    font-family: var(--font-mono);
    font-size: 0.65rem;
    letter-spacing: 0.15em;
    text-transform: uppercase;
    color: var(--text3);
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: space-between;
  }

  .toc-list {
    flex: 1;
    overflow-y: auto;
    padding: 10px 0;
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
  }

  .toc-item {
    display: block;
    padding: 5px 16px;
    font-size: 0.82rem;
    color: var(--text2);
    text-decoration: none;
    font-family: var(--font-body);
    line-height: 1.4;
    border-left: 2px solid transparent;
    transition: all 0.15s;
    cursor: pointer;
  }
  .toc-item:hover { color: var(--text); background: var(--bg3); }
  .toc-item.active { color: var(--accent); border-left-color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
  .toc-h2 { padding-left: 26px; font-size: 0.79rem; }
  .toc-h3 { padding-left: 38px; font-size: 0.75rem; opacity: 0.85; }
  .toc-h4 { padding-left: 50px; font-size: 0.72rem; opacity: 0.75; }

  /* Stats bar */
  .stats-bar {
    padding: 10px 16px;
    border-top: 1px solid var(--border);
    font-family: var(--font-mono);
    font-size: 0.67rem;
    color: var(--text3);
    display: flex;
    flex-direction: column;
    gap: 3px;
  }

  /* ── Main content ── */
  .main-area {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    position: relative;
  }

  /* Drop zone overlay */
  .drop-overlay {
    position: absolute;
    inset: 0;
    background: color-mix(in srgb, var(--accent) 10%, var(--bg));
    border: 3px dashed var(--accent);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 100;
    font-family: var(--font-display);
    font-size: 1.5rem;
    color: var(--accent);
    pointer-events: none;
  }
  .drop-overlay.active { display: flex; }

  /* Tab bar */
  .tab-bar {
    display: flex;
    align-items: center;
    background: var(--bg2);
    border-bottom: 1px solid var(--border);
    padding: 0 12px;
    gap: 4px;
    min-height: 40px;
    overflow-x: auto;
    scrollbar-width: none;
  }
  .tab-bar::-webkit-scrollbar { display: none; }

  .tab {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border-radius: 6px 6px 0 0;
    font-family: var(--font-mono);
    font-size: 0.72rem;
    color: var(--text3);
    cursor: pointer;
    border: 1px solid transparent;
    border-bottom: none;
    margin-bottom: -1px;
    white-space: nowrap;
    transition: all 0.15s;
    max-width: 160px;
  }
  .tab:hover { color: var(--text2); background: var(--bg3); }
  .tab.active {
    color: var(--text);
    background: var(--bg);
    border-color: var(--border);
    border-bottom-color: var(--bg);
  }
  .tab .tab-close {
    width: 14px; height: 14px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 3px;
    opacity: 0.5;
  }
  .tab .tab-close:hover { opacity: 1; background: var(--accent2); color: #fff; }
  .tab-name { overflow: hidden; text-overflow: ellipsis; }

  .tab-bar-end { margin-left: auto; flex-shrink: 0; }

  /* Content split: editor + preview */
  .content-area {
    display: flex;
    flex: 1;
    overflow: hidden;
    position: relative;
  }

  .editor-pane {
    display: none;
    flex-direction: column;
    border-right: 1px solid var(--border);
    background: var(--surface);
  }
  .editor-pane.show { display: flex; }

  .editor-header {
    padding: 6px 12px;
    font-family: var(--font-mono);
    font-size: 0.65rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text3);
    border-bottom: 1px solid var(--border);
    background: var(--bg2);
  }

  .editor-textarea {
    flex: 1;
    background: var(--surface);
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 0.85rem;
    line-height: 1.7;
    border: none;
    outline: none;
    resize: none;
    padding: 20px;
    tab-size: 2;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
  }

  .preview-pane {
    flex: 1;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
    background: var(--bg);
    display: flex;
    flex-direction: column;
  }

  /* Split ratio */
  .content-area.split .editor-pane { flex: 1; display: flex; }
  .content-area.split .preview-pane { flex: 1; }
  .content-area.editor-only .editor-pane { flex: 1; display: flex; }
  .content-area.editor-only .preview-pane { display: none; }

  /* ── Prose rendering ── */
  .prose-container {
    max-width: var(--prose-width);
    margin: 0 auto;
    padding: 48px 32px 80px;
    width: 100%;
    flex: 1;
  }

  .prose-container.wide { --prose-width: 100%; padding: 32px 40px; }

  /* Progress indicator */
  .read-progress {
    position: sticky;
    top: 0;
    height: 2px;
    background: var(--border);
    z-index: 5;
  }
  .read-progress-bar {
    height: 100%;
    background: linear-gradient(90deg, var(--accent), var(--accent2));
    width: 0%;
    transition: width 0.1s;
  }

  /* Prose styles */
  .prose { font-size: 1rem; line-height: 1.85; color: var(--text); }
  .prose.sm { font-size: 0.9rem; }
  .prose.lg { font-size: 1.1rem; }
  .prose.xl { font-size: 1.2rem; }

  .prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 {
    font-family: var(--font-display);
    color: var(--text);
    line-height: 1.25;
    margin-top: 2.2em;
    margin-bottom: 0.7em;
  }
  .prose h1 { font-size: 2.4em; letter-spacing: -0.02em; border-bottom: 2px solid var(--accent); padding-bottom: 0.25em; margin-top: 0; }
  .prose h2 { font-size: 1.7em; letter-spacing: -0.01em; }
  .prose h2::before { content: '§'; color: var(--accent); margin-right: 0.5em; font-style: italic; opacity: 0.6; font-size: 0.8em; }
  .prose h3 { font-size: 1.3em; font-style: italic; }
  .prose h4 { font-size: 1.1em; font-family: var(--font-mono); font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; font-size: 0.9em; color: var(--accent); }
  .prose h5, .prose h6 { font-size: 1em; }

  .prose p { margin-bottom: 1.4em; }
  .prose p:last-child { margin-bottom: 0; }

  .prose a { color: var(--accent); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent); text-underline-offset: 3px; transition: all 0.15s; }
  .prose a:hover { text-decoration-color: var(--accent); }

  .prose strong { font-weight: 700; color: var(--text); }
  .prose em { font-style: italic; color: var(--text); }
  .prose del { opacity: 0.5; text-decoration: line-through; }

  .prose code {
    font-family: var(--font-mono);
    font-size: 0.85em;
    background: var(--bg3);
    color: var(--accent3);
    padding: 0.15em 0.45em;
    border-radius: 4px;
    border: 1px solid var(--border);
  }

  .prose pre {
    background: var(--bg2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin: 1.5em 0;
    overflow-x: auto;
    position: relative;
  }
  .prose pre code {
    background: none;
    border: none;
    padding: 0;
    color: inherit;
    font-size: 0.87em;
    display: block;
    padding: 18px 20px;
    line-height: 1.65;
  }

  .code-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 8px 14px;
    border-bottom: 1px solid var(--border);
    font-family: var(--font-mono);
    font-size: 0.7rem;
    color: var(--text3);
  }
  .code-lang-badge {
    background: var(--accent);
    color: var(--bg);
    padding: 2px 7px;
    border-radius: 4px;
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.05em;
  }
  .copy-btn {
    cursor: pointer;
    padding: 3px 8px;
    border-radius: 4px;
    border: 1px solid var(--border);
    background: transparent;
    color: var(--text3);
    font-family: var(--font-mono);
    font-size: 0.67rem;
    transition: all 0.15s;
  }
  .copy-btn:hover { color: var(--accent); border-color: var(--accent); }
  .copy-btn.copied { color: var(--accent3); border-color: var(--accent3); }

  .prose blockquote {
    border-left: 3px solid var(--accent);
    padding: 2px 0 2px 20px;
    margin: 1.5em 0;
    color: var(--text2);
    font-style: italic;
    position: relative;
  }
  .prose blockquote::before {
    content: '"';
    font-family: var(--font-display);
    font-size: 3em;
    color: var(--accent);
    opacity: 0.2;
    position: absolute;
    top: -10px;
    left: 10px;
    line-height: 1;
  }
  .prose blockquote p { margin: 0; }

  .prose ul, .prose ol { padding-left: 1.6em; margin-bottom: 1.4em; }
  .prose li { margin-bottom: 0.4em; }
  .prose li > ul, .prose li > ol { margin-top: 0.4em; margin-bottom: 0; }

  /* Task list */
  .prose input[type="checkbox"] {
    appearance: none;
    width: 15px;
    height: 15px;
    border: 2px solid var(--border);
    border-radius: 3px;
    margin-right: 7px;
    vertical-align: middle;
    cursor: default;
    position: relative;
    top: -1px;
    flex-shrink: 0;
  }
  .prose input[type="checkbox"]:checked {
    background: var(--accent);
    border-color: var(--accent);
  }
  .prose input[type="checkbox"]:checked::after {
    content: '✓';
    font-size: 10px;
    color: var(--bg);
    position: absolute;
    top: -1px;
    left: 1px;
  }

  .prose table {
    width: 100%;
    border-collapse: collapse;
    margin: 1.5em 0;
    font-size: 0.9em;
    overflow-x: auto;
    display: block;
  }
  .prose th {
    background: var(--bg2);
    font-family: var(--font-mono);
    font-size: 0.78em;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    color: var(--text2);
    padding: 10px 14px;
    border: 1px solid var(--border);
    text-align: left;
    font-weight: 600;
  }
  .prose td {
    padding: 9px 14px;
    border: 1px solid var(--border);
    vertical-align: top;
  }
  .prose tr:nth-child(even) td { background: color-mix(in srgb, var(--bg2) 40%, transparent); }
  .prose tr:hover td { background: color-mix(in srgb, var(--accent) 5%, transparent); }

  .prose hr {
    border: none;
    border-top: 2px solid var(--border);
    margin: 3em 0;
    position: relative;
  }
  .prose hr::after {
    content: '◈';
    position: absolute;
    top: -0.7em;
    left: 50%;
    transform: translateX(-50%);
    background: var(--bg);
    padding: 0 10px;
    color: var(--accent);
    font-size: 0.8em;
  }

  .prose img {
    max-width: 100%;
    border-radius: var(--radius);
    border: 1px solid var(--border);
    margin: 1em 0;
  }

  /* Footnotes */
  .prose .footnotes {
    border-top: 1px solid var(--border);
    margin-top: 3em;
    padding-top: 1em;
    font-size: 0.85em;
    color: var(--text2);
  }

  /* KaTeX */
  .prose .katex-display { margin: 1.5em 0; overflow-x: auto; }

  /* Mermaid */
  .prose .mermaid {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 20px;
    text-align: center;
    margin: 1.5em 0;
    overflow-x: auto;
  }
  .prose .mermaid svg { max-width: 100%; }

  /* Definition list */
  .prose dl { margin: 1.5em 0; }
  .prose dt { font-family: var(--font-mono); font-size: 0.85em; color: var(--accent); margin-top: 1em; }
  .prose dd { margin-left: 1.5em; color: var(--text2); }

  /* Highlight mark */
  .prose mark { background: color-mix(in srgb, var(--accent) 30%, transparent); color: var(--text); padding: 0.05em 0.2em; border-radius: 2px; }

  /* Emoji sizing */
  .prose img.emoji { width: 1.2em; height: 1.2em; border: none; vertical-align: middle; margin: 0 0.05em; }

  /* Welcome screen */
  .welcome {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 40px;
    text-align: center;
    gap: 12px;
    color: var(--text3);
  }
  .welcome-icon {
    font-size: 3rem;
    margin-bottom: 8px;
    filter: grayscale(0.3);
  }
  .welcome h2 {
    font-family: var(--font-display);
    font-size: 1.6rem;
    color: var(--text2);
    font-weight: 400;
    font-style: italic;
  }
  .welcome p { font-size: 0.88rem; max-width: 400px; line-height: 1.7; }
  .welcome-actions { display: flex; gap: 10px; margin-top: 8px; }
  .welcome-btn {
    padding: 8px 18px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--bg2);
    color: var(--text2);
    font-family: var(--font-mono);
    font-size: 0.78rem;
    cursor: pointer;
    transition: all 0.18s;
  }
  .welcome-btn:hover { border-color: var(--accent); color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, var(--bg2)); }
  .welcome-btn.primary { background: var(--accent); color: var(--bg); border-color: var(--accent); }
  .welcome-btn.primary:hover { opacity: 0.9; }

  /* Scrollbar */
  ::-webkit-scrollbar { width: 6px; height: 6px; }
  ::-webkit-scrollbar-track { background: transparent; }
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
  ::-webkit-scrollbar-thumb:hover { background: var(--text3); }

  /* Search overlay */
  .search-overlay {
    position: absolute;
    top: 10px;
    right: 10px;
    background: var(--bg2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 10px 12px;
    display: none;
    align-items: center;
    gap: 8px;
    z-index: 50;
    box-shadow: 0 8px 24px color-mix(in srgb, #000 40%, transparent);
  }
  .search-overlay.show { display: flex; }
  .search-input {
    background: var(--bg3);
    border: 1px solid var(--border);
    border-radius: 5px;
    color: var(--text);
    font-family: var(--font-mono);
    font-size: 0.82rem;
    padding: 5px 10px;
    outline: none;
    width: 200px;
  }
  .search-input:focus { border-color: var(--accent); }
  .search-count { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text3); white-space: nowrap; }

  /* Print */
  @media print {
    .sidebar, header, .tab-bar, .read-progress, .search-overlay { display: none !important; }
    .app { display: block; }
    .prose-container { max-width: 100%; padding: 0; }
    .prose { color: #000; }
  }

  /* Responsive */
  @media (max-width: 768px) {
    .app { grid-template-columns: 0 1fr; }
    .sidebar { display: none; }
    .hide-mobile { display: none; }
  }

  /* Animation */
  @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
  .prose { animation: fadeIn 0.3s ease; }
</style>
</head>
<body>
<div class="app" id="app">
  <!-- Header -->
  <header>
    <div class="logo">Mark<span>Flow</span></div>
    <div class="header-actions hide-mobile" style="gap:4px">
      <button class="btn btn-icon" id="toggleSidebar" title="Toggle Sidebar">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
      </button>
    </div>
    <div class="header-sep"></div>
    <div class="header-actions">
      <!-- View mode -->
      <div class="font-size-group hide-mobile">
        <button class="btn btn-icon" id="modePreview" title="Preview only">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
        </button>
        <button class="btn btn-icon" id="modeSplit" title="Split view">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></svg>
        </button>
        <button class="btn btn-icon" id="modeEditor" title="Editor only">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
        </button>
      </div>

      <!-- Font size -->
      <div class="font-size-group hide-mobile">
        <button class="btn" id="fontSmall" data-size="sm" title="Small text">A</button>
        <button class="btn active" id="fontMed" data-size="" title="Medium text" style="font-size:0.85rem">A</button>
        <button class="btn" id="fontLarge" data-size="lg" title="Large text" style="font-size:1rem">A</button>
      </div>

      <!-- Wide mode -->
      <button class="btn btn-icon hide-mobile" id="toggleWide" title="Wide mode">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
      </button>

      <!-- Search -->
      <button class="btn btn-icon" id="searchBtn" title="Search (Ctrl+F)">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
      </button>

      <!-- Theme -->
      <select class="theme-select hide-mobile" id="themeSelect">
        <option value="">Dark</option>
        <option value="light">Light</option>
        <option value="sepia">Sepia</option>
        <option value="forest">Forest</option>
      </select>

      <!-- Open file -->
      <label class="btn" title="Open file" style="cursor:pointer">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
        Open
        <input type="file" id="fileInput" accept=".md,.markdown,.txt" multiple style="display:none">
      </label>

      <!-- Print -->
      <button class="btn btn-icon hide-mobile" id="printBtn" title="Print / Export PDF">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
      </button>
    </div>
  </header>

  <!-- Sidebar -->
  <aside class="sidebar">
    <div class="sidebar-header">
      <span>Table of Contents</span>
    </div>
    <nav class="toc-list" id="tocList">
      <div style="padding:20px 16px; font-size:0.8rem; color:var(--text3); font-style:italic;">Open a Markdown file to see the table of contents.</div>
    </nav>
    <div class="stats-bar" id="statsBar" style="display:none">
      <span id="statWords">0 words</span>
      <span id="statChars">0 chars</span>
      <span id="statRead">~0 min read</span>
    </div>
  </aside>

  <!-- Main -->
  <main class="main-area" id="mainArea">
    <div class="drop-overlay" id="dropOverlay">📄 Drop Markdown files here</div>

    <!-- Search overlay -->
    <div class="search-overlay" id="searchOverlay">
      <input type="text" class="search-input" id="searchInput" placeholder="Search...">
      <span class="search-count" id="searchCount"></span>
      <button class="copy-btn" id="searchPrev">↑</button>
      <button class="copy-btn" id="searchNext">↓</button>
      <button class="copy-btn" id="searchClose">✕</button>
    </div>

    <!-- Tab bar -->
    <div class="tab-bar" id="tabBar">
      <div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--text3);padding:0 4px;">no files open</div>
      <div class="tab-bar-end">
        <button class="btn" id="newTabBtn" style="font-size:0.7rem">+ New</button>
      </div>
    </div>

    <!-- Content -->
    <div class="content-area" id="contentArea">
      <div class="editor-pane" id="editorPane">
        <div class="editor-header">Markdown Source</div>
        <textarea class="editor-textarea" id="editorTextarea" spellcheck="false" placeholder="Type or paste Markdown here..."></textarea>
      </div>
      <div class="preview-pane" id="previewPane">
        <div class="read-progress"><div class="read-progress-bar" id="progressBar"></div></div>
        <div class="prose-container" id="proseContainer">
          <!-- Welcome screen -->
          <div class="welcome" id="welcomeScreen">
            <div class="welcome-icon">📖</div>
            <h2>Beautiful Markdown Reading</h2>
            <p>Open any Markdown file, drag &amp; drop, or start typing in the editor. Supports LaTeX math, code highlighting, Mermaid diagrams, tables, task lists, and more.</p>
            <div class="welcome-actions">
              <button class="welcome-btn primary" id="loadDemo">Load Demo</button>
              <label class="welcome-btn" style="cursor:pointer">Open File<input type="file" accept=".md,.markdown,.txt" multiple style="display:none" onchange="handleFiles(this.files)"></label>
            </div>
          </div>
          <article class="prose" id="proseContent" style="display:none"></article>
        </div>
      </div>
    </div>
  </main>
</div>

<script>
// ─────────────────────────────────────────────
// State
// ─────────────────────────────────────────────
let tabs = [];
let activeTabId = null;
let viewMode = 'preview'; // preview | split | editor-only
let wideMode = false;
let fontSize = '';
let searchMatches = [];
let searchIndex = 0;

// ─────────────────────────────────────────────
// Marked configuration
// ─────────────────────────────────────────────
marked.setOptions({
  breaks: true,
  gfm: true,
});

// Custom renderer
const renderer = new marked.Renderer();

// Code blocks with language badge and copy button
renderer.code = function(code, lang) {
  const language = lang || 'text';
  const validLang = hljs.getLanguage(language) ? language : 'plaintext';
  let highlighted;
  try {
    highlighted = language === 'mermaid'
      ? `<div class="mermaid">${code}</div>`
      : hljs.highlight(code, { language: validLang }).value;
  } catch(e) {
    highlighted = code;
  }
  if (language === 'mermaid') return highlighted;
  const id = 'cb-' + Math.random().toString(36).slice(2,8);
  return `<pre>
    <div class="code-header">
      <span class="code-lang-badge">${language.toUpperCase()}</span>
      <button class="copy-btn" onclick="copyCode(this,'${id}')">Copy</button>
    </div>
    <code class="hljs language-${validLang}" id="${id}">${highlighted}</code>
  </pre>`;
};

// Task list items
renderer.listitem = function(text, task, checked) {
  if (task) {
    return `<li style="list-style:none;margin-left:-1em"><label><input type="checkbox" ${checked?'checked':''} disabled> ${text}</label></li>`;
  }
  return `<li>${text}</li>`;
};

// Heading with anchor
renderer.heading = function(text, level) {
  const slug = text.toLowerCase().replace(/[^\w]+/g,'-').replace(/^-|-$/g,'');
  return `<h${level} id="${slug}">${text}</h${level}>`;
};

marked.use({ renderer });

// ─────────────────────────────────────────────
// Rendering
// ─────────────────────────────────────────────
function renderMarkdown(md) {
  let html = marked.parse(md);
  return html;
}

function postProcess(container) {
  // KaTeX
  if (window.renderMathInElement) {
    renderMathInElement(container, {
      delimiters: [
        {left: '$$', right: '$$', display: true},
        {left: '$', right: '$', display: false},
        {left: '\\(', right: '\\)', display: false},
        {left: '\\[', right: '\\]', display: true},
      ],
      throwOnError: false,
    });
  }
  // Mermaid
  if (window.mermaid) {
    mermaid.run({ nodes: container.querySelectorAll('.mermaid') });
  }
}

function updatePreview(md) {
  const prose = document.getElementById('proseContent');
  const welcome = document.getElementById('welcomeScreen');
  if (!md && md !== '') {
    welcome.style.display = 'flex';
    prose.style.display = 'none';
    return;
  }
  welcome.style.display = 'none';
  prose.style.display = 'block';
  prose.innerHTML = renderMarkdown(md);
  postProcess(prose);
  buildTOC(md);
  updateStats(md);
}

// ─────────────────────────────────────────────
// TOC
// ─────────────────────────────────────────────
function buildTOC(md) {
  const headingRe = /^(#{1,4})\s+(.+)$/gm;
  const items = [];
  let m;
  while ((m = headingRe.exec(md)) !== null) {
    items.push({ level: m[1].length, text: m[2].trim() });
  }
  const tocList = document.getElementById('tocList');
  if (!items.length) {
    tocList.innerHTML = '<div style="padding:16px;font-size:0.8rem;color:var(--text3);font-style:italic;">No headings found.</div>';
    return;
  }
  tocList.innerHTML = items.map(h => {
    const slug = h.text.toLowerCase().replace(/[^\w]+/g,'-').replace(/^-|-$/g,'');
    const cls = ['toc-item', `toc-h${h.level}`].join(' ');
    return `<a class="${cls}" onclick="scrollToHeading('${slug}')">${h.text}</a>`;
  }).join('');
}

function scrollToHeading(slug) {
  const el = document.getElementById(slug);
  if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
}

// ─────────────────────────────────────────────
// Stats
// ─────────────────────────────────────────────
function updateStats(md) {
  const statsBar = document.getElementById('statsBar');
  const words = md.trim().split(/\s+/).filter(Boolean).length;
  const chars = md.length;
  const readMin = Math.max(1, Math.round(words / 200));
  document.getElementById('statWords').textContent = words.toLocaleString() + ' words';
  document.getElementById('statChars').textContent = chars.toLocaleString() + ' chars';
  document.getElementById('statRead').textContent = `~${readMin} min read`;
  statsBar.style.display = 'flex';
}

// ─────────────────────────────────────────────
// Tabs
// ─────────────────────────────────────────────
function createTab(name, content) {
  const id = 'tab-' + Date.now() + '-' + Math.random().toString(36).slice(2,5);
  tabs.push({ id, name, content });
  renderTabs();
  switchTab(id);
}

function switchTab(id) {
  activeTabId = id;
  const tab = tabs.find(t => t.id === id);
  renderTabs();
  if (tab) {
    document.getElementById('editorTextarea').value = tab.content;
    updatePreview(tab.content);
  }
}

function closeTab(id) {
  tabs = tabs.filter(t => t.id !== id);
  if (activeTabId === id) {
    activeTabId = tabs.length ? tabs[tabs.length-1].id : null;
    if (activeTabId) switchTab(activeTabId);
    else {
      document.getElementById('editorTextarea').value = '';
      updatePreview(null);
      document.getElementById('tocList').innerHTML = '<div style="padding:20px 16px;font-size:0.8rem;color:var(--text3);font-style:italic;">Open a Markdown file to see the table of contents.</div>';
      document.getElementById('statsBar').style.display = 'none';
    }
  }
  renderTabs();
}

function renderTabs() {
  const bar = document.getElementById('tabBar');
  if (!tabs.length) {
    bar.innerHTML = '<div style="font-family:var(--font-mono);font-size:0.7rem;color:var(--text3);padding:0 4px;">no files open</div><div class="tab-bar-end"><button class="btn" id="newTabBtn" style="font-size:0.7rem" onclick="newTab()">+ New</button></div>';
    return;
  }
  const tabsHtml = tabs.map(t => {
    const active = t.id === activeTabId ? ' active' : '';
    return `<div class="tab${active}" onclick="switchTab('${t.id}')">
      <span class="tab-name" title="${t.name}">${t.name}</span>
      <span class="tab-close" onclick="event.stopPropagation();closeTab('${t.id}')">✕</span>
    </div>`;
  }).join('');
  bar.innerHTML = tabsHtml + `<div class="tab-bar-end"><button class="btn" style="font-size:0.7rem" onclick="newTab()">+ New</button></div>`;
}

function newTab() {
  createTab('Untitled.md', '');
  setViewMode('split');
}

// ─────────────────────────────────────────────
// File handling
// ─────────────────────────────────────────────
function handleFiles(files) {
  Array.from(files).forEach(file => {
    const reader = new FileReader();
    reader.onload = e => createTab(file.name, e.target.result);
    reader.readAsText(file);
  });
}

document.getElementById('fileInput').addEventListener('change', e => handleFiles(e.target.files));

// Drag & drop
const mainArea = document.getElementById('mainArea');
const dropOverlay = document.getElementById('dropOverlay');
mainArea.addEventListener('dragover', e => { e.preventDefault(); dropOverlay.classList.add('active'); });
mainArea.addEventListener('dragleave', e => { if (!mainArea.contains(e.relatedTarget)) dropOverlay.classList.remove('active'); });
mainArea.addEventListener('drop', e => {
  e.preventDefault();
  dropOverlay.classList.remove('active');
  handleFiles(e.dataTransfer.files);
});

// Paste from clipboard
document.addEventListener('paste', e => {
  if (document.activeElement === document.getElementById('editorTextarea')) return;
  const text = e.clipboardData.getData('text');
  if (text && text.length > 50) createTab('Pasted.md', text);
});

// ─────────────────────────────────────────────
// Editor sync
// ─────────────────────────────────────────────
let renderTimeout;
document.getElementById('editorTextarea').addEventListener('input', function() {
  if (!activeTabId) return;
  const tab = tabs.find(t => t.id === activeTabId);
  if (tab) tab.content = this.value;
  clearTimeout(renderTimeout);
  renderTimeout = setTimeout(() => updatePreview(this.value), 250);
});

// ─────────────────────────────────────────────
// View mode
// ─────────────────────────────────────────────
function setViewMode(mode) {
  viewMode = mode;
  const ca = document.getElementById('contentArea');
  const ep = document.getElementById('editorPane');
  ca.className = 'content-area';
  if (mode === 'split') { ca.classList.add('split'); ep.classList.add('show'); }
  else if (mode === 'editor-only') { ca.classList.add('editor-only'); ep.classList.add('show'); }
  else { ep.classList.remove('show'); }

  document.getElementById('modePreview').classList.toggle('active', mode==='preview');
  document.getElementById('modeSplit').classList.toggle('active', mode==='split');
  document.getElementById('modeEditor').classList.toggle('active', mode==='editor-only');
}

document.getElementById('modePreview').onclick = () => setViewMode('preview');
document.getElementById('modeSplit').onclick = () => setViewMode('split');
document.getElementById('modeEditor').onclick = () => setViewMode('editor-only');

// ─────────────────────────────────────────────
// Sidebar toggle
// ─────────────────────────────────────────────
document.getElementById('toggleSidebar').onclick = () => {
  document.getElementById('app').classList.toggle('no-sidebar');
};

// ─────────────────────────────────────────────
// Wide mode
// ─────────────────────────────────────────────
document.getElementById('toggleWide').onclick = function() {
  wideMode = !wideMode;
  document.getElementById('proseContainer').classList.toggle('wide', wideMode);
  this.classList.toggle('active', wideMode);
};

// ─────────────────────────────────────────────
// Font size
// ─────────────────────────────────────────────
const fontBtns = ['fontSmall','fontMed','fontLarge'];
fontBtns.forEach(btnId => {
  document.getElementById(btnId).onclick = function() {
    fontSize = this.dataset.size;
    const prose = document.getElementById('proseContent');
    prose.className = 'prose ' + fontSize;
    fontBtns.forEach(id => document.getElementById(id).classList.remove('active'));
    this.classList.add('active');
  };
});

// ─────────────────────────────────────────────
// Theme
// ─────────────────────────────────────────────
document.getElementById('themeSelect').onchange = function() {
  document.documentElement.setAttribute('data-theme', this.value);
  // Switch hljs theme
  const hljsLink = document.getElementById('hljs-theme');
  if (this.value === 'light' || this.value === 'sepia') {
    hljsLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css';
  } else if (this.value === 'forest') {
    hljsLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/monokai-sublime.min.css';
  } else {
    hljsLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/tokyo-night-dark.min.css';
  }
};

// ─────────────────────────────────────────────
// Print
// ─────────────────────────────────────────────
document.getElementById('printBtn').onclick = () => window.print();

// ─────────────────────────────────────────────
// Search
// ─────────────────────────────────────────────
document.getElementById('searchBtn').onclick = () => {
  document.getElementById('searchOverlay').classList.toggle('show');
  if (document.getElementById('searchOverlay').classList.contains('show')) {
    document.getElementById('searchInput').focus();
  }
};
document.getElementById('searchClose').onclick = () => {
  document.getElementById('searchOverlay').classList.remove('show');
  clearSearch();
};
document.getElementById('searchInput').oninput = doSearch;
document.getElementById('searchNext').onclick = () => navigateSearch(1);
document.getElementById('searchPrev').onclick = () => navigateSearch(-1);

function doSearch() {
  clearSearch();
  const q = document.getElementById('searchInput').value.trim();
  if (!q) { document.getElementById('searchCount').textContent = ''; return; }
  const prose = document.getElementById('proseContent');
  const text = prose.innerHTML;
  // Highlight matches
  const re = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi');
  prose.innerHTML = prose.innerHTML.replace(re, m => `<mark class="search-highlight" style="background:var(--accent);color:var(--bg);border-radius:2px">${m}</mark>`);
  searchMatches = Array.from(prose.querySelectorAll('.search-highlight'));
  document.getElementById('searchCount').textContent = searchMatches.length ? `1 / ${searchMatches.length}` : 'No results';
  searchIndex = 0;
  if (searchMatches.length) searchMatches[0].scrollIntoView({ behavior:'smooth', block:'center' });
}

function navigateSearch(dir) {
  if (!searchMatches.length) return;
  searchIndex = (searchIndex + dir + searchMatches.length) % searchMatches.length;
  searchMatches.forEach((m,i) => m.style.background = i===searchIndex ? 'var(--accent2)' : 'var(--accent)');
  searchMatches[searchIndex].scrollIntoView({ behavior:'smooth', block:'center' });
  document.getElementById('searchCount').textContent = `${searchIndex+1} / ${searchMatches.length}`;
}

function clearSearch() {
  const prose = document.getElementById('proseContent');
  prose.querySelectorAll('.search-highlight').forEach(el => {
    el.replaceWith(document.createTextNode(el.textContent));
  });
  searchMatches = [];
}

// Keyboard shortcuts
document.addEventListener('keydown', e => {
  if ((e.ctrlKey || e.metaKey) && e.key === 'f') { e.preventDefault(); document.getElementById('searchBtn').click(); }
  if (e.key === 'Escape') {
    document.getElementById('searchOverlay').classList.remove('show');
    clearSearch();
  }
  if (e.key === 'Enter' && document.getElementById('searchOverlay').classList.contains('show')) {
    navigateSearch(e.shiftKey ? -1 : 1);
  }
});

// ─────────────────────────────────────────────
// Reading progress
// ─────────────────────────────────────────────
document.getElementById('previewPane').addEventListener('scroll', function() {
  const { scrollTop, scrollHeight, clientHeight } = this;
  const pct = scrollHeight > clientHeight ? (scrollTop / (scrollHeight - clientHeight)) * 100 : 0;
  document.getElementById('progressBar').style.width = pct + '%';
  // Highlight active TOC item
  const headings = document.getElementById('proseContent').querySelectorAll('h1,h2,h3,h4');
  let active = null;
  headings.forEach(h => { if (h.offsetTop - 80 <= scrollTop + 10) active = h.id; });
  document.querySelectorAll('.toc-item').forEach(el => el.classList.remove('active'));
  if (active) {
    const tocItem = document.querySelector(`.toc-item[onclick*="${active}"]`);
    if (tocItem) tocItem.classList.add('active');
  }
});

// ─────────────────────────────────────────────
// Copy code
// ─────────────────────────────────────────────
window.copyCode = function(btn, id) {
  const el = document.getElementById(id);
  if (!el) return;
  navigator.clipboard.writeText(el.innerText).then(() => {
    btn.textContent = '✓ Copied';
    btn.classList.add('copied');
    setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1800);
  });
};

// ─────────────────────────────────────────────
// Mermaid init
// ─────────────────────────────────────────────
mermaid.initialize({
  startOnLoad: false,
  theme: 'dark',
  themeVariables: { darkMode: true, background: 'transparent' },
  securityLevel: 'loose',
});

// ─────────────────────────────────────────────
// Demo content
// ─────────────────────────────────────────────
const DEMO = `# MarkFlow Demo

> A powerful, beautiful Markdown reader with everything you need.

## Typography & Formatting

**Bold text**, *italic text*, ~~strikethrough~~, and \`inline code\`.

You can use ==highlighted text== with mark tags, and create [links](https://example.com).

---

## Math Formulas

Inline math: $E = mc^2$ and $\\sin^2\\theta + \\cos^2\\theta = 1$

Display math:

$$
\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}
$$

Maxwell's equations:

$$\\nabla \\cdot \\mathbf{E} = \\frac{\\rho}{\\varepsilon_0}$$

## Code Highlighting

\`\`\`python
def fibonacci(n: int) -> list[int]:
    """Generate Fibonacci sequence up to n."""
    seq = [0, 1]
    while seq[-1] + seq[-2] < n:
        seq.append(seq[-1] + seq[-2])
    return seq

result = fibonacci(100)
print(f"Found {len(result)} numbers: {result[:5]}...")
\`\`\`

\`\`\`javascript
const debounce = (fn, delay) => {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
};
\`\`\`

## Diagrams (Mermaid)

\`\`\`mermaid
graph TD
    A[Start] --> B{Is it Markdown?}
    B -->|Yes| C[Render beautifully]
    B -->|No| D[Convert first]
    C --> E[Read & enjoy]
    D --> C
\`\`\`

## Tables

| Feature | Support | Notes |
|---------|:-------:|-------|
| GFM Tables | ✅ | Full support |
| Task Lists | ✅ | Interactive checkboxes |
| Math (KaTeX) | ✅ | Inline & block |
| Mermaid | ✅ | Flow, sequence, Gantt |
| Code Highlighting | ✅ | 190+ languages |
| Footnotes | ✅ | Numbered refs |

## Task Lists

- [x] Build beautiful Markdown reader
- [x] Add math formula support
- [x] Implement syntax highlighting
- [x] Add Mermaid diagram support
- [ ] Add collaborative editing
- [ ] Mobile app version

## Blockquotes

> "The best way to predict the future is to invent it."
>
> --- Alan Kay

## Footnotes

The quick brown fox[^1] jumps over the lazy dog[^2].

[^1]: The fox is known for its agility.
[^2]: The dog is just having a relaxing day.

---

*MarkFlow --- Beautiful Markdown, every time.*
`;

document.getElementById('loadDemo').onclick = () => createTab('demo.md', DEMO);
</script>
</body>
</html>
相关推荐
姜太小白1 天前
【VSCode/Trae】trae已安装的扩展如何导出
ide·vscode·编辑器
进击的横打2 天前
【车载开发系列】Renesas Flash Programmer (RFP) 反向读取功能
车载系统·编辑器·rfp
山峰哥2 天前
数据库工程中的SQL调优实践:从索引策略到查询优化的深度探索
服务器·数据库·sql·性能优化·编辑器
CodeQingqing2 天前
cubemx + Keil + vscode + Keil Assistant 工作流
ide·vscode·编辑器·keil
yuezhilangniao3 天前
【AI 编辑器开发规范 v2.1 版】—— 为 AI 时代的敏捷开发而生
人工智能·编辑器·敏捷流程
charlie1145141914 天前
从0开始榨干 Claude Code:VSCode 实战配置与默认读取文件完整踩坑记录
ide·vscode·编辑器
智算菩萨5 天前
马年奔腾,万象更新——2026新年祝福与马年文化深度解读
编辑器
山峰哥5 天前
SQL调优实战:从索引失效到性能飙升的破局之道
服务器·数据库·sql·性能优化·编辑器·深度优先
山峰哥6 天前
数据库调优实战:索引策略与查询优化案例解析
服务器·数据库·sql·性能优化·编辑器