Download index.html from SupaC/md-viewer: direct link, hf CLI and curl.
- Browser
- Download file 18 kB
-
https://huggingface.co/spaces/SupaC/md-viewer/resolve/main/index.html
- Command line
-
hf download hf://spaces/SupaC/md-viewer/index.html
-
curl -L -o index.html https://huggingface.co/spaces/SupaC/md-viewer/resolve/main/index.html
18 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>MD Viewer β markdown into text + code snippets</title> | |
| <style> | |
| :root { --bg:#0f172a; --panel:#1e293b; --panel2:#020617; --line:#334155; --txt:#e2e8f0; --mut:#94a3b8; --acc:#38bdf8; --grn:#34d399; } | |
| * { box-sizing: border-box; } | |
| body { margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background:var(--bg); color:var(--txt); } | |
| header { padding:20px 24px 0; max-width:1280px; margin:0 auto; } | |
| h1 { margin:0; font-size:1.5rem; display:flex; align-items:center; gap:.6rem; } | |
| h1 .ico { font-size:1.7rem; } | |
| .sub { color:var(--mut); font-size:.9rem; margin:.35rem 0 1rem; } | |
| .wrap { max-width:1280px; margin:0 auto; padding:0 24px 40px; } | |
| .toolbar { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:14px; display:flex; flex-wrap:wrap; gap:10px; align-items:center; } | |
| .search { flex:1; min-width:220px; display:flex; align-items:center; gap:8px; background:var(--panel2); border:1px solid var(--line); border-radius:10px; padding:8px 12px; } | |
| .search input { flex:1; background:transparent; border:0; outline:0; color:var(--txt); font-size:.9rem; } | |
| .search input::placeholder { color:var(--mut); } | |
| select, .btn { background:var(--panel2); color:var(--txt); border:1px solid var(--line); border-radius:10px; padding:8px 12px; font-size:.85rem; cursor:pointer; } | |
| .btn:hover { border-color:var(--acc); } | |
| .seg { display:flex; border:1px solid var(--line); border-radius:10px; overflow:hidden; } | |
| .seg button { background:var(--panel2); color:var(--mut); border:0; padding:8px 14px; font-size:.85rem; cursor:pointer; text-transform:capitalize; } | |
| .seg button.on { background:var(--acc); color:#06283a; font-weight:600; } | |
| .chk { display:flex; align-items:center; gap:6px; font-size:.8rem; color:var(--mut); cursor:pointer; user-select:none; } | |
| .chk input { accent-color:var(--acc); } | |
| .stats { display:flex; flex-wrap:wrap; gap:16px; font-size:.78rem; color:var(--mut); margin-top:10px; } | |
| .grid { display:grid; grid-template-columns:280px 1fr; gap:16px; margin-top:16px; } | |
| @media (max-width:900px){ .grid{ grid-template-columns:1fr; } } | |
| .side { display:flex; flex-direction:column; gap:16px; } | |
| .card { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:14px; } | |
| .card h3 { margin:0 0 8px; font-size:.85rem; } | |
| .card input[type=text], .card textarea { width:100%; background:var(--panel2); border:1px solid var(--line); border-radius:10px; color:var(--txt); font-size:.8rem; padding:8px 10px; outline:none; } | |
| .card textarea { font-family:ui-monospace, monospace; font-size:.72rem; resize:vertical; } | |
| .card input:focus, .card textarea:focus { border-color:var(--acc); } | |
| .filelist { max-height:220px; overflow:auto; display:flex; flex-direction:column; gap:2px; margin-top:8px; } | |
| .filelist button { text-align:left; background:transparent; border:0; color:var(--mut); font-family:ui-monospace,monospace; font-size:.72rem; padding:6px 8px; border-radius:8px; cursor:pointer; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } | |
| .filelist button:hover { background:#33415555; color:var(--txt); } | |
| .filelist button.on { background:#38bdf822; color:var(--acc); } | |
| .cols { display:grid; gap:16px; } | |
| .cols.split { grid-template-columns:1fr 1fr; } | |
| @media (max-width:1100px){ .cols.split{ grid-template-columns:1fr; } } | |
| .sech { font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--mut); margin:0 0 8px; } | |
| .tcard { background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:14px; margin-bottom:12px; font-size:.88rem; line-height:1.6; } | |
| .tcard .hd { font-size:.68rem; font-weight:700; text-transform:uppercase; letter-spacing:.05em; color:var(--acc); margin-bottom:6px; } | |
| .tcard p { margin:.35rem 0; color:#cbd5e1; } | |
| .tcard .mh { font-weight:700; color:var(--txt); margin:.3rem 0; } | |
| .tcard .mh2{font-size:1.05rem}.tcard .mh3{font-size:1rem}.tcard .mh4{font-size:.92rem} | |
| .tcard li { margin:.2rem 0 .2rem 1rem; color:#cbd5e1; } | |
| .tcard blockquote { border-left:3px solid var(--acc); margin:.4rem 0; padding-left:.6rem; opacity:.85; } | |
| .tcard code.ic { font-family:ui-monospace,monospace; font-size:.8em; background:#38bdf822; padding:.05rem .35rem; border-radius:.35rem; } | |
| .scard { background:#000; border:1px solid var(--line); border-radius:12px; overflow:hidden; margin-bottom:12px; } | |
| .scard .bar { display:flex; align-items:center; justify-content:space-between; gap:8px; padding:8px 12px; border-bottom:1px solid #1e293b; } | |
| .lang { font-family:ui-monospace,monospace; font-size:.68rem; font-weight:700; background:#34d39922; color:var(--grn); padding:2px 8px; border-radius:6px; } | |
| .rel { font-size:.68rem; color:var(--mut); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } | |
| .copy { background:transparent; border:0; color:var(--mut); font-size:.72rem; cursor:pointer; flex-shrink:0; } | |
| .copy:hover { color:#fff; } | |
| .scard pre { margin:0; padding:12px; font-family:ui-monospace,monospace; font-size:.75rem; line-height:1.55; color:#e2e8f0; overflow:auto; max-height:380px; white-space:pre; } | |
| .empty { font-size:.85rem; color:var(--mut); border:1px dashed var(--line); border-radius:12px; padding:16px; } | |
| .err { background:#7f1d1d55; border:1px solid #ef4444; color:#fca5a5; border-radius:12px; padding:10px 14px; font-size:.85rem; margin-top:12px; display:none; } | |
| footer { text-align:center; color:var(--mut); font-size:.75rem; padding:20px; } | |
| footer a { color:var(--acc); } | |
| #dropov { position:fixed; inset:0; display:none; align-items:center; justify-content:center; background:#38bdf822; border:3px dashed var(--acc); border-radius:18px; margin:12px; z-index:50; font-size:1.1rem; font-weight:600; color:var(--acc); pointer-events:none; } | |
| #dropov.show { display:flex; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="dropov">Drop the .md file to open it</div> | |
| <header> | |
| <h1><span class="ico">π</span> MD Viewer</h1> | |
| <p class="sub">Open any Markdown β it is filtered and broken down into <b>text blocks</b> and <b>relevant code snippets</b>.</p> | |
| </header> | |
| <div class="wrap"> | |
| <div class="toolbar"> | |
| <div class="search">π <input id="q" type="text" placeholder="Filter text + code (e.g. deploy, docker, api)β¦" /></div> | |
| <select id="lang" title="Filter snippets by language"><option value="all">All languages</option></select> | |
| <div class="seg" id="viewseg"> | |
| <button data-v="split" class="on">Split</button><button data-v="text">Text</button><button data-v="code">Code</button> | |
| </div> | |
| <label class="chk" title="Keep blocks sharing a heading with any match"><input type="checkbox" id="rel" checked /> Relevant only</label> | |
| </div> | |
| <div class="stats" id="stats"></div> | |
| <div class="err" id="err"></div> | |
| <div class="grid"> | |
| <div class="side"> | |
| <div class="card"> | |
| <h3>π Sample docs</h3> | |
| <div class="filelist" id="samples"></div> | |
| </div> | |
| <div class="card"> | |
| <h3>π Load from URL</h3> | |
| <input type="text" id="url" placeholder="https://raw.githubusercontent.com/β¦/README.md" /> | |
| <button class="btn" id="fetchbtn" style="margin-top:8px;width:100%">Fetch</button> | |
| </div> | |
| <div class="card"> | |
| <h3>π€ Paste / upload</h3> | |
| <textarea id="paste" rows="10" spellcheck="false"></textarea> | |
| <div style="display:flex;gap:8px;margin-top:8px"> | |
| <label class="btn" style="flex:1;text-align:center">Upload<input type="file" id="up" accept=".md,.markdown,.txt" hidden /></label> | |
| <button class="btn" id="reset" style="flex:1">Sample</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="cols split" id="cols"> | |
| <section id="textsec"><p class="sech">π Text</p><div id="texts"></div></section> | |
| <section id="codesec"><p class="sech">π» Snippets</p><div id="codes"></div></section> | |
| </div> | |
| </div> | |
| </div> | |
| <footer>MD Viewer Β· Static Space Β· <a href="https://huggingface.co/spaces/SupaC/md-viewer">SupaC/md-viewer</a></footer> | |
| <script> | |
| ; | |
| const $ = id => document.getElementById(id); | |
| const SAMPLE = `# MD Viewer | |
| Paste or open any \`.md\` file. This viewer **filters** the document and breaks it into **text cards** and **code snippets**. | |
| ## How it works | |
| - Text blocks keep their heading context | |
| - Code fences become copyable snippets with language badges | |
| - Search keeps only relevant text + its related code | |
| ## Example | |
| Inline \`code\` renders inside text. Lists work too: | |
| - First point | |
| - Second point | |
| \`\`\`ts | |
| function greet(name) { | |
| return \`hello \${name}\`; | |
| } | |
| \`\`\` | |
| \`\`\`bash | |
| npm run dev | |
| \`\`\` | |
| `; | |
| const SAMPLE_DEPLOY = `# Deploy Guide | |
| Steps to ship the app to production. | |
| ## Prerequisites | |
| You need **docker** and an SSH key on the server. | |
| - Docker 24+ | |
| - 2 GB free RAM | |
| ## Deploy | |
| Run the deploy script, then check health: | |
| \`\`\`bash | |
| ./deploy.sh production | |
| curl -f http://localhost:3000/health | |
| \`\`\` | |
| ## Config | |
| Set env vars before deploying: | |
| \`\`\`env | |
| PORT=3000 | |
| DATABASE_URL=postgres://db:5432/app | |
| \`\`\` | |
| ## API | |
| Health endpoint returns JSON: | |
| \`\`\`ts | |
| const res = await fetch("/health"); | |
| const body = await res.json(); | |
| \`\`\` | |
| `; | |
| const state = { raw: SAMPLE, name: "sample doc", q: "", lang: "all", view: "split", rel: true }; | |
| function esc(s){ return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); } | |
| function inline(s){ | |
| let h = esc(s); | |
| h = h.replace(/`([^`]+)`/g, '<code class="ic">$1</code>'); | |
| h = h.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"); | |
| h = h.replace(/(^|\s)\*([^*\n]+)\*/g, "$1<em>$2</em>"); | |
| return h; | |
| } | |
| function renderText(md){ | |
| const out = []; | |
| for (const raw of md.split("\n")) { | |
| const line = raw.replace(/\s+$/,""); | |
| if (!line.trim()) continue; | |
| const hm = line.match(/^(#{1,6})\s+(.*)$/); | |
| if (hm) { const l = Math.min(hm[1].length+1,4); out.push(`<div class="mh mh${l}">${inline(hm[2])}</div>`); continue; } | |
| const t = line.trim(); | |
| if (/^[-*]\s+/.test(t)) { out.push(`<li>${inline(t.replace(/^[-*]\s+/,""))}</li>`); continue; } | |
| if (/^\d+\.\s+/.test(t)) { out.push(`<li>${inline(t)}</li>`); continue; } | |
| if (/^>\s?/.test(t)) { out.push(`<blockquote>${inline(t.replace(/^>\s?/,""))}</blockquote>`); continue; } | |
| out.push(`<p>${inline(line)}</p>`); | |
| } | |
| return out.join(""); | |
| } | |
| function parseMarkdown(src){ | |
| const lines = src.replace(/\r\n/g,"\n").split("\n"); | |
| const texts = [], codes = []; | |
| let buf = [], curH = null, curL = 0, n = 0; | |
| const flush = () => { | |
| const content = buf.join("\n").trim(); buf = []; | |
| if (!content) return; | |
| let cur = { h: curH, l: curL, lines: [] }; | |
| const push = () => { const c = cur.lines.join("\n").trim(); if (c) { n++; texts.push({id:"t"+n, heading:cur.h, level:cur.l, content:c}); } }; | |
| for (const line of content.split("\n")) { | |
| const m = line.match(/^(#{1,6})\s+(.*)$/); | |
| if (m) { push(); curH = m[2].trim(); curL = m[1].length; cur = { h: curH, l: curL, lines: [line] }; } | |
| else cur.lines.push(line); | |
| } | |
| push(); | |
| }; | |
| let inCode = false, lang = "text", cbuf = [], ch = null; | |
| for (const line of lines) { | |
| const f = line.match(/^```\s*([A-Za-z0-9_+#.-]*)\s*$/); | |
| if (f && !inCode) { flush(); inCode = true; lang = (f[1]||"text").toLowerCase()||"text"; cbuf = []; ch = curH; continue; } | |
| if (f && inCode) { n++; codes.push({id:"c"+n, lang, code:cbuf.join("\n"), rel:ch}); inCode = false; cbuf = []; continue; } | |
| if (inCode) cbuf.push(line); | |
| else { const m = line.match(/^(#{1,6})\s+(.*)$/); if (m){ curH = m[2].trim(); curL = m[1].length; } buf.push(line); } | |
| } | |
| if (inCode) { n++; codes.push({id:"c"+n, lang, code:cbuf.join("\n"), rel:ch}); } else flush(); | |
| return { texts, codes }; | |
| } | |
| function applyFilter(p){ | |
| const q = state.q.trim().toLowerCase(); | |
| let codes = state.lang === "all" ? p.codes : p.codes.filter(b => b.lang === state.lang); | |
| let texts = p.texts; | |
| if (!q) return { texts, codes }; | |
| const th = b => (b.heading||"").toLowerCase().includes(q) || b.content.toLowerCase().includes(q); | |
| const ch2 = b => b.code.toLowerCase().includes(q) || b.lang.includes(q) || (b.rel||"").toLowerCase().includes(q); | |
| const mt = texts.filter(th), mc = codes.filter(ch2); | |
| if (!state.rel) return { texts: mt, codes: mc }; | |
| const heads = new Set(); | |
| mt.forEach(b => b.heading && heads.add(b.heading)); | |
| mc.forEach(b => b.rel && heads.add(b.rel)); | |
| return { | |
| texts: texts.filter(b => th(b) || (b.heading && heads.has(b.heading))), | |
| codes: codes.filter(b => ch2(b) || (b.rel && heads.has(b.rel))) | |
| }; | |
| } | |
| function render(){ | |
| const p = parseMarkdown(state.raw); | |
| const langs = [...new Set(p.codes.map(b=>b.lang))].sort(); | |
| const sel = $("lang"), cur = sel.value; | |
| sel.innerHTML = `<option value="all">All languages (${langs.length})</option>` + langs.map(l=>`<option value="${esc(l)}">${esc(l)}</option>`).join(""); | |
| sel.value = langs.includes(cur) ? cur : "all"; | |
| if (langs.includes(cur)) state.lang = cur; else state.lang = "all"; | |
| const f = applyFilter(p); | |
| $("stats").innerHTML = `<span>π ${f.texts.length} text blocks</span><span>π» ${f.codes.length} snippets</span><span>π ${esc(state.name)}</span>` + | |
| (state.q.trim() ? `<span>filter: β${esc(state.q.trim())}β${state.rel ? " (+ related)" : " (exact)"}</span>` : ""); | |
| document.querySelectorAll("#viewseg button").forEach(b => b.classList.toggle("on", b.dataset.v === state.view)); | |
| const cols = $("cols"); | |
| cols.className = "cols" + (state.view === "split" ? " split" : ""); | |
| $("textsec").style.display = state.view === "code" ? "none" : ""; | |
| $("codesec").style.display = state.view === "text" ? "none" : ""; | |
| $("texts").innerHTML = f.texts.length ? f.texts.map(b => | |
| `<article class="tcard">${b.heading ? `<div class="hd">${esc(b.heading)}</div>` : ""}${renderText(b.content)}</article>` | |
| ).join("") : `<div class="empty">No text matches this filter.</div>`; | |
| $("codes").innerHTML = f.codes.length ? f.codes.map(b => | |
| `<article class="scard"><div class="bar"><div style="display:flex;gap:8px;align-items:center;min-width:0"><span class="lang">${esc(b.lang)}</span>${b.rel ? `<span class="rel">Β§ ${esc(b.rel)}</span>` : ""}</div><button class="copy" data-id="${b.id}">β§ Copy</button></div><pre>${esc(b.code) || '<span style="color:#475569">(empty)</span>'}</pre></article>` | |
| ).join("") : `<div class="empty">No snippets match this filter.</div>`; | |
| document.querySelectorAll(".copy").forEach(btn => btn.addEventListener("click", () => { | |
| const card = btn.closest(".scard"); | |
| const code = card.querySelector("pre").innerText; | |
| navigator.clipboard.writeText(code).then(() => { | |
| const o = btn.textContent; btn.textContent = "β Copied"; | |
| setTimeout(() => btn.textContent = o, 1200); | |
| }); | |
| })); | |
| } | |
| function setRaw(raw, name){ state.raw = raw; state.name = name; $("paste").value = raw; render(); } | |
| function showErr(m){ const e = $("err"); e.textContent = m; e.style.display = m ? "block" : "none"; } | |
| let qt; $("q").addEventListener("input", e => { clearTimeout(qt); qt = setTimeout(() => { state.q = e.target.value; render(); }, 200); }); | |
| $("lang").addEventListener("change", e => { state.lang = e.target.value; render(); }); | |
| $("rel").addEventListener("change", e => { state.rel = e.target.checked; render(); }); | |
| document.querySelectorAll("#viewseg button").forEach(b => b.addEventListener("click", () => { state.view = b.dataset.v; render(); })); | |
| $("paste").addEventListener("input", e => { state.raw = e.target.value; state.name = "pasted doc"; render(); }); | |
| $("up").addEventListener("change", e => { | |
| const f = e.target.files[0]; if (!f) return; | |
| const r = new FileReader(); | |
| r.onload = () => setRaw(String(r.result || ""), f.name); | |
| r.readAsText(f); e.target.value = ""; | |
| }); | |
| $("reset").addEventListener("click", () => setRaw(SAMPLE, "sample doc")); | |
| async function doFetch(u) { | |
| showErr(""); | |
| try { | |
| const r = await fetch(u); | |
| if (!r.ok) throw new Error("HTTP " + r.status); | |
| const text = await r.text(); | |
| setRaw(text, (u.split("/").pop() || u).split("?")[0] || u); | |
| try { history.replaceState(null, "", "?url=" + encodeURIComponent(u)); } catch (e) {} | |
| } catch (e) { showErr("Fetch failed (URL may block cross-origin requests): " + e.message); } | |
| } | |
| $("fetchbtn").addEventListener("click", () => { | |
| const u = $("url").value.trim(); if (!u) return; | |
| doFetch(u); | |
| }); | |
| // Drag & drop a local .md file anywhere onto the app | |
| window.addEventListener("dragover", e => { e.preventDefault(); $("dropov").classList.add("show"); }); | |
| window.addEventListener("dragleave", e => { if (!e.relatedTarget) $("dropov").classList.remove("show"); }); | |
| window.addEventListener("drop", e => { | |
| e.preventDefault(); $("dropov").classList.remove("show"); | |
| const f = [...(e.dataTransfer.files || [])].find(x => /\.(md|markdown|mdown|txt)$/i.test(x.name)); | |
| if (!f) { showErr("Drop a .md file to view it"); return; } | |
| const r = new FileReader(); | |
| r.onload = () => { setRaw(String(r.result || ""), f.name); try { history.replaceState(null, "", location.pathname); } catch (e2) {} }; | |
| r.readAsText(f); | |
| }); | |
| const samples = [["Sample doc", SAMPLE], ["Deploy guide", SAMPLE_DEPLOY]]; | |
| const sl = $("samples"); | |
| samples.forEach(([name, body], i) => { | |
| const b = document.createElement("button"); | |
| b.textContent = name; if (i === 0) b.classList.add("on"); | |
| b.addEventListener("click", () => { | |
| sl.querySelectorAll("button").forEach(x => x.classList.remove("on")); | |
| b.classList.add("on"); setRaw(body, name.toLowerCase().replace(/ /g,"-") + ".md"); | |
| }); | |
| sl.appendChild(b); | |
| }); | |
| $("paste").value = SAMPLE; | |
| render(); | |
| // Deep-link: ?url=<raw-markdown-url> auto-loads that doc | |
| try { | |
| const deep = new URLSearchParams(location.search).get("url"); | |
| if (deep) { $("url").value = deep; doFetch(deep); } | |
| } catch (e) {} | |
| </script> | |
| </body> | |
| </html> | |