Last active
February 7, 2026 20:42
-
-
Save rahuldhole/78c425c1b44302cdfad40865a85b74b4 to your computer and use it in GitHub Desktop.
Publish your markdown docs/ folder with a single html file
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <!-- | |
| DocsViewer | |
| Source: https://gist.github.com/rahuldhole/78c425c1b44302cdfad40865a85b74b4 | |
| This file a readymade template to quick & dirty host markdown files. | |
| Usage: | |
| - add your markdown files in some public folder. | |
| - add links of your markdown files in the `sidebarMenu` array. | |
| - add links of your markdown files in the `topBar` array. | |
| --> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <title>DocsViewer</title> | |
| <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/primeicons@7.0.0/primeicons.min.css"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.5.1/github-markdown-dark.min.css" /> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/tokyo-night-dark.min.css" /> | |
| <script> | |
| window.MathJax = { | |
| tex: { inlineMath: [['$', '$'], ['\\(', '\\)']], displayMath: [['$$', '$$']] }, | |
| options: { ignoreHtmlClass: 'tex2jax_ignore', processHtmlClass: 'tex2jax_process' } | |
| }; | |
| </script> | |
| <script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/marked@12.0.1/lib/marked.umd.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.0/dist/mermaid.min.js"></script> | |
| <style> | |
| body { opacity: 0; transition: opacity 0.2s; background-color: #0d1117; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; } | |
| .markdown-body { background: transparent !important; font-family: inherit; line-height: 1.6; } | |
| .mermaid { background: #161b22; padding: 1.5rem; border-radius: 12px; display: flex; justify-content: center; margin: 1.5rem 0; border: 1px solid #30363d; overflow-x: auto; } | |
| .markdown-body pre { background-color: #1a1b26 !important; border: 1px solid #30363d; border-radius: 12px; position: relative; } | |
| .mdiv { background: rgba(56, 189, 248, 0.1); border-left: 4px solid #38bdf8; padding: 1rem 1.5rem; margin: 1.5rem 0; border-radius: 0.5rem; color: #e2e8f0; font-size: 0.95em; } | |
| .mdiv::before { content: attr(data-label); display: block; font-size: 0.7rem; font-weight: 700; text-transform: uppercase; color: #38bdf8; margin-bottom: 0.5rem; opacity: 0.8; } | |
| .submenu-grid { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 0.2s ease-out; } | |
| .submenu-grid.open { grid-template-rows: 1fr; } | |
| .submenu-content { overflow: hidden; } | |
| .chevron { transition: transform 0.2s ease; } | |
| .chevron.rotate { transform: rotate(90deg); } | |
| /* Scrollbar styling */ | |
| ::-webkit-scrollbar { width: 6px; height: 6px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: #30363d; border-radius: 10px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #58a6ff; } | |
| .loader { border: 2px solid #30363d; border-top: 2px solid #58a6ff; border-radius: 50%; width: 20px; height: 20px; animation: spin 1s linear infinite; } | |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| /* TOC Specific Animations */ | |
| .toc-item-enter-active, .toc-item-leave-active { transition: all 0.2s ease; max-height: 500px; opacity: 1; overflow: hidden; } | |
| .toc-item-enter-from, .toc-item-leave-to { max-height: 0; opacity: 0; } | |
| </style> | |
| <script type="importmap"> | |
| { "imports": { "petite-vue": "https://unpkg.com/petite-vue@0.4.1/dist/petite-vue.es.js" } } | |
| </script> | |
| </head> | |
| <body @vue:mounted="init" class="flex flex-col h-screen text-gray-300 overflow-hidden bg-[#0d1117]"> | |
| <!-- Header --> | |
| <header class="h-14 bg-[#161b22]/95 backdrop-blur-md border-b border-[#30363d] flex items-center justify-between px-4 sticky top-0 z-50 shrink-0"> | |
| <div class="flex items-center gap-3"> | |
| <!-- Mobile Menu Toggle --> | |
| <button class="md:hidden text-gray-400 hover:text-white p-1" @click="sidebarOpen = !sidebarOpen" v-if="!isSingleDoc"> | |
| <i class="pi pi-bars text-lg"></i> | |
| </button> | |
| <!-- Desktop Sidebar Toggle --> | |
| <button @click="desktopSidebarVisible = !desktopSidebarVisible" v-if="!isSingleDoc" class="hidden md:flex items-center justify-center w-8 h-8 text-gray-400 hover:text-white rounded-md hover:bg-[#1f242c] transition-colors mr-1"> | |
| <i class="pi" :class="desktopSidebarVisible ? 'pi-align-left' : 'pi-align-justify'"></i> | |
| </button> | |
| <div class="flex items-center gap-2"> | |
| <div class="w-7 h-7 bg-blue-500/10 rounded-lg flex items-center justify-center text-blue-400"> | |
| <i class="pi pi-book text-sm"></i> | |
| </div> | |
| <span class="font-bold text-white tracking-tight text-sm sm:text-base">DocsViewer</span> | |
| <span class="text-[10px] text-neutral-500 font-bold uppercase tracking-widest mt-1 hidden sm:block">Publish markdown</span> | |
| </div> | |
| </div> | |
| <!-- Top Navigation --> | |
| <div class="flex items-center gap-4"> | |
| <!-- Mobile TOC Toggle --> | |
| <button class="xl:hidden text-gray-400 hover:text-blue-400 p-1" @click="mobileTocOpen = !mobileTocOpen" v-if="toc.length > 0"> | |
| <i class="pi pi-list text-lg"></i> | |
| </button> | |
| <nav class="hidden md:flex items-center gap-6 text-sm font-medium"> | |
| <a v-for="link in topBar" :href="link.link" class="hover:text-blue-400 transition-colors flex items-center gap-2 text-gray-400"> | |
| <i :class="link.icon" v-if="link.icon.includes('pi-')"></i> | |
| <span v-else class="font-bold opacity-70">{{link.icon}}</span> | |
| <span>{{link.text}}</span> | |
| </a> | |
| </nav> | |
| </div> | |
| </header> | |
| <!-- Main Workspace --> | |
| <div class="flex flex-1 overflow-hidden relative"> | |
| <!-- Mobile Backdrop for Nav Sidebar --> | |
| <div v-if="sidebarOpen" @click="sidebarOpen = false" class="fixed inset-0 bg-black/60 z-30 md:hidden transition-opacity"></div> | |
| <!-- Left Navigation Sidebar --> | |
| <aside | |
| v-if="!isSingleDoc" | |
| class="bg-[#0d1117] border-r border-[#30363d] flex flex-col absolute md:relative h-full z-40 shadow-2xl md:shadow-none overflow-hidden transition-all duration-300 ease-in-out" | |
| :class="[ | |
| sidebarOpen ? 'translate-x-0 w-72' : '-translate-x-full w-72', /* Mobile State */ | |
| desktopSidebarVisible ? 'md:translate-x-0 md:w-72' : 'md:w-0 md:border-r-0' /* Desktop State */ | |
| ]" | |
| > | |
| <div class="p-4 flex flex-col h-full w-72"> | |
| <!-- Search Bar --> | |
| <div class="mb-4"> | |
| <div class="relative"> | |
| <i class="pi pi-search absolute left-3 top-2.5 text-gray-500 text-xs"></i> | |
| <input | |
| v-model="searchQuery" | |
| placeholder="Filter docs..." | |
| class="w-full bg-[#161b22] border border-[#30363d] rounded-md py-1.5 pl-8 pr-3 text-xs text-gray-300 focus:border-blue-500 focus:outline-none placeholder-gray-600 transition-colors" | |
| > | |
| </div> | |
| </div> | |
| <div class="overflow-y-auto flex-1 custom-scrollbar pb-6"> | |
| <div class="text-[10px] font-bold text-gray-500 uppercase tracking-[0.2em] mb-3 px-2">Explorer</div> | |
| <div class="space-y-1"> | |
| <div v-for="item in filteredSidebarMenu" :key="item.id || item.title"> | |
| <!-- Regular Link --> | |
| <button v-if="!item.submenu" @click="loadPage(item.link, item.id)" | |
| :class="currentUrl === item.link ? 'bg-blue-500/10 text-blue-400' : 'text-gray-400 hover:bg-[#1f242c] hover:text-gray-100'" | |
| class="w-full text-left px-3 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-3"> | |
| <i class="pi pi-file text-xs opacity-50"></i> | |
| <span class="truncate" v-html="highlightText(item.title)"></span> | |
| </button> | |
| <!-- Submenu --> | |
| <div v-else> | |
| <button @click="toggleSubmenu(item.title)" | |
| class="w-full text-left px-3 py-2 rounded-lg text-sm font-medium text-gray-400 hover:bg-[#1f242c] hover:text-gray-100 transition-all flex items-center justify-between group"> | |
| <div class="flex items-center gap-3"> | |
| <i class="pi pi-folder text-xs opacity-50"></i> | |
| <span v-html="highlightText(item.title)"></span> | |
| </div> | |
| <i class="pi pi-chevron-right text-[10px] opacity-40 group-hover:opacity-80 transition-transform chevron" | |
| :class="{ 'rotate': searchQuery || openSubmenus.includes(item.title) }"></i> | |
| </button> | |
| <div class="submenu-grid" :class="{ 'open': searchQuery || openSubmenus.includes(item.title) }"> | |
| <div class="submenu-content pl-6 pt-1 space-y-1"> | |
| <button v-for="sub in item.submenu" :key="sub.id || sub.title" @click="loadPage(sub.link, sub.id)" | |
| :class="currentUrl === sub.link ? 'text-blue-400' : 'text-gray-500 hover:text-gray-200'" | |
| class="w-full text-left px-3 py-1.5 text-[13px] transition-colors truncate"> | |
| <span v-html="highlightText(sub.title)"></span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div v-if="filteredSidebarMenu.length === 0" class="text-center py-4 text-xs text-gray-600 italic"> | |
| No matching documents | |
| </div> | |
| </div> | |
| <!-- Mobile Only Top Bar Links --> | |
| <div class="md:hidden mt-6 pt-6 border-t border-[#30363d] space-y-2"> | |
| <div class="text-[10px] font-bold text-gray-500 uppercase tracking-[0.2em] mb-2 px-2">Links</div> | |
| <a v-for="link in topBar" :href="link.link" class="block px-3 py-2 text-sm text-gray-400 hover:text-white rounded-lg hover:bg-[#1f242c] transition-colors flex items-center gap-3"> | |
| <i :class="link.icon" v-if="link.icon.includes('pi-')"></i> | |
| <span v-else class="font-bold opacity-70">{{link.icon}}</span> | |
| <span>{{link.text}}</span> | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Center Content & Right TOC Wrapper --> | |
| <div class="flex flex-1 overflow-hidden relative"> | |
| <!-- Main Content Scroll Area --> | |
| <main id="main-scroll" class="flex-1 overflow-y-auto bg-[#0d1117] scroll-smooth relative"> | |
| <div class="max-w-4xl mx-auto px-6 py-10 md:px-12 md:py-16 min-h-full"> | |
| <div v-if="loading" class="flex flex-col items-center justify-center py-32 opacity-60"> | |
| <div class="loader mb-4"></div> | |
| <p class="text-xs font-mono tracking-widest uppercase">Fetching</p> | |
| </div> | |
| <div v-else-if="error" class="p-8 rounded-2xl border border-red-500/20 bg-red-500/5 text-center mt-10"> | |
| <i class="pi pi-exclamation-circle text-3xl text-red-400/50 mb-4"></i> | |
| <h3 class="font-bold text-red-400">Unable to load document</h3> | |
| <p class="text-sm text-gray-500 mt-2">{{error}}</p> | |
| <button @click="loadPage(currentUrl)" class="mt-6 text-xs bg-red-500/10 hover:bg-red-500/20 text-red-400 px-4 py-2 rounded-full transition-colors">Retry Connection</button> | |
| </div> | |
| <div v-show="!loading && !error" id="content" class="markdown-body pb-20"></div> | |
| </div> | |
| </main> | |
| <!-- Right TOC Sidebar (Desktop) --> | |
| <aside | |
| v-if="toc.length > 0" | |
| class="hidden xl:block w-72 border-l border-[#30363d] bg-[#0d1117]/50 backdrop-blur-sm overflow-y-auto shrink-0" | |
| > | |
| <div class="p-6 fixed w-72 top-14 bottom-0 overflow-y-auto custom-scrollbar"> | |
| <h5 class="text-xs font-bold text-gray-500 uppercase tracking-widest mb-4">On this page</h5> | |
| <!-- Recursive TOC Render --> | |
| <ul class="space-y-1"> | |
| <li v-for="item in toc" :key="item.id" class="relative"> | |
| <div class="flex items-start group"> | |
| <!-- Expand/Collapse Chevron for Parents --> | |
| <button | |
| v-if="item.children && item.children.length > 0" | |
| @click.stop="item.expanded = !item.expanded" | |
| class="mt-1 mr-1 p-0.5 rounded hover:bg-gray-800 text-gray-500 transition-colors" | |
| > | |
| <i class="pi pi-chevron-right text-[10px] block transition-transform duration-200" :class="{ 'rotate-90': item.expanded }"></i> | |
| </button> | |
| <!-- Spacer if no children to align text --> | |
| <div v-else class="w-4 mr-1"></div> | |
| <a | |
| :href="'#' + item.id" | |
| @click.prevent="scrollToId(item.id)" | |
| class="text-sm py-0.5 block transition-colors leading-snug hover:text-gray-200" | |
| :class="activeTocId === item.id ? 'text-blue-400 font-medium' : 'text-gray-400'" | |
| > | |
| {{ item.text }} | |
| </a> | |
| </div> | |
| <!-- Children (2nd Level) --> | |
| <div v-if="item.children && item.children.length > 0" | |
| class="grid transition-[grid-template-rows] duration-200 ease-out ml-2 border-l border-gray-800" | |
| :class="item.expanded ? 'grid-rows-[1fr] mt-1 mb-2' : 'grid-rows-[0fr]'"> | |
| <ul class="overflow-hidden"> | |
| <li v-for="child in item.children" :key="child.id" class="pl-4 py-1"> | |
| <a | |
| :href="'#' + child.id" | |
| @click.prevent="scrollToId(child.id)" | |
| class="text-xs block transition-colors hover:text-gray-300 truncate" | |
| :class="activeTocId === child.id ? 'text-blue-400' : 'text-gray-500'" | |
| > | |
| {{ child.text }} | |
| </a> | |
| </li> | |
| </ul> | |
| </div> | |
| </li> | |
| </ul> | |
| </div> | |
| </aside> | |
| <!-- Mobile TOC Drawer --> | |
| <div v-if="mobileTocOpen" class="xl:hidden absolute inset-0 z-50 flex justify-end"> | |
| <div class="absolute inset-0 bg-black/60" @click="mobileTocOpen = false"></div> | |
| <div class="w-72 bg-[#161b22] h-full shadow-2xl relative flex flex-col border-l border-[#30363d] animate-slide-in"> | |
| <div class="p-4 border-b border-[#30363d] flex justify-between items-center"> | |
| <span class="text-sm font-bold text-gray-300">Table of Contents</span> | |
| <button @click="mobileTocOpen = false" class="text-gray-400 hover:text-white"> | |
| <i class="pi pi-times"></i> | |
| </button> | |
| </div> | |
| <div class="p-4 overflow-y-auto flex-1"> | |
| <ul class="space-y-3"> | |
| <li v-for="item in toc" :key="item.id"> | |
| <a :href="'#' + item.id" @click="mobileTocOpen = false; scrollToId(item.id)" | |
| class="block text-sm font-medium" | |
| :class="activeTocId === item.id ? 'text-blue-400' : 'text-gray-400'"> | |
| {{ item.text }} | |
| </a> | |
| <ul v-if="item.children && item.children.length" class="mt-2 ml-4 space-y-2 border-l border-gray-700 pl-3"> | |
| <li v-for="child in item.children" :key="child.id"> | |
| <a :href="'#' + child.id" @click="mobileTocOpen = false; scrollToId(child.id)" | |
| class="block text-xs" | |
| :class="activeTocId === child.id ? 'text-blue-400' : 'text-gray-500'"> | |
| {{ child.text }} | |
| </a> | |
| </li> | |
| </ul> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import { createApp } from 'petite-vue'; | |
| createApp({ | |
| // UI States | |
| sidebarOpen: false, | |
| desktopSidebarVisible: true, | |
| mobileTocOpen: false, | |
| // Data States | |
| loading: false, | |
| error: null, | |
| currentUrl: '', | |
| openSubmenus: [], | |
| searchQuery: '', | |
| // TOC States | |
| toc: [], | |
| activeTocId: '', | |
| observer: null, | |
| topBar: [ | |
| { text: "Repository", icon: "pi pi-github", link: "#" }, | |
| { text: "Discord", icon: "pi pi-discord", link: "#" }, | |
| { text: "v1.3", icon: "📦", link: "#" } | |
| ], | |
| sidebarMenu: [ | |
| { | |
| title: "Introduction", | |
| id: "introduction", | |
| link: "https://gist.githubusercontent.com/rahuldhole/35450c3fc2c5b63ec9ad9745d6bc918a/raw/00ec3d91ac9222832a9929c9b902cd75ec13667f/markdown-sample-fully-featured.md" | |
| }, | |
| { | |
| title: "Guides", | |
| submenu: [ | |
| { | |
| title: "Basics", | |
| id: "basics", | |
| link: "https://gist.githubusercontent.com/rahuldhole/35450c3fc2c5b63ec9ad9745d6bc918a/raw/5ffe4ec7c6d84cf35df0822635239cd8771e14c5/markdown-sample-1.md" | |
| }, | |
| { | |
| title: "Advanced", | |
| id: "advanced", | |
| link: "https://gist.githubusercontent.com/rahuldhole/35450c3fc2c5b63ec9ad9745d6bc918a/raw/5ffe4ec7c6d84cf35df0822635239cd8771e14c5/markdown-sample-2.md" | |
| } | |
| ] | |
| } | |
| ], | |
| get isSingleDoc() { | |
| if (!this.sidebarMenu || this.sidebarMenu.length === 0) return false; | |
| if (this.sidebarMenu.length > 1) return false; | |
| return !this.sidebarMenu[0].submenu; | |
| }, | |
| get filteredSidebarMenu() { | |
| if (!this.searchQuery) return this.sidebarMenu; | |
| const q = this.searchQuery.toLowerCase(); | |
| return this.sidebarMenu.map(item => { | |
| let filteredSub = []; | |
| if (item.submenu) { | |
| filteredSub = item.submenu.filter(s => s.title.toLowerCase().includes(q)); | |
| } | |
| const matchesParent = item.title.toLowerCase().includes(q); | |
| if (matchesParent) return item; | |
| if (filteredSub.length > 0) return { ...item, submenu: filteredSub }; | |
| return null; | |
| }).filter(Boolean); | |
| }, | |
| init() { | |
| requestAnimationFrame(() => { | |
| mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' }); | |
| marked.setOptions({ gfm: true, breaks: true }); | |
| this.checkHashAndLoad(); | |
| document.body.style.opacity = '1'; | |
| }); | |
| }, | |
| // --- TOC Logic --- | |
| generateToc() { | |
| const content = document.getElementById('content'); | |
| if (!content) return; | |
| const headers = Array.from(content.querySelectorAll('h1, h2, h3')); | |
| const tocData = []; | |
| let currentParent = null; | |
| headers.forEach((h, index) => { | |
| // Ensure ID | |
| if (!h.id) { | |
| h.id = 'heading-' + index + '-' + h.innerText.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); | |
| } | |
| const level = parseInt(h.tagName.substring(1)); | |
| const item = { | |
| id: h.id, | |
| text: h.innerText, | |
| level, | |
| children: [], | |
| expanded: true | |
| }; | |
| // Simple Logic: | |
| // H1 and H2 are top level. H3 is child of previous H2. | |
| if (level <= 2) { | |
| tocData.push(item); | |
| currentParent = item; | |
| } else if (currentParent) { | |
| currentParent.children.push(item); | |
| } else { | |
| // Orphan H3 (starts doc with H3), treat as top level | |
| tocData.push(item); | |
| currentParent = item; | |
| } | |
| }); | |
| this.toc = tocData; | |
| this.setupIntersectionObserver(); | |
| }, | |
| setupIntersectionObserver() { | |
| if (this.observer) this.observer.disconnect(); | |
| const options = { | |
| root: document.getElementById('main-scroll'), | |
| rootMargin: '-80px 0px -80% 0px', // Active when element is near top | |
| threshold: 0 | |
| }; | |
| this.observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| this.activeTocId = entry.target.id; | |
| // Auto expand parent if child is active | |
| this.toc.forEach(parent => { | |
| if (parent.children.some(child => child.id === this.activeTocId)) { | |
| // Optional: parent.expanded = true; | |
| } | |
| }); | |
| } | |
| }); | |
| }, options); | |
| const content = document.getElementById('content'); | |
| content.querySelectorAll('h1, h2, h3').forEach(h => { | |
| this.observer.observe(h); | |
| }); | |
| }, | |
| scrollToId(id) { | |
| const el = document.getElementById(id); | |
| if (el) { | |
| el.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| // Update hash without jump | |
| history.pushState(null, null, '#' + id); | |
| this.activeTocId = id; | |
| } | |
| }, | |
| // --- Navigation & Content Loading --- | |
| findPathToId(items, id, path = []) { | |
| for (const item of items) { | |
| if (item.id === id) return [...path, item]; | |
| if (item.submenu) { | |
| const found = this.findPathToId(item.submenu, id, [...path, item.title]); | |
| if (found) return found; | |
| } | |
| } | |
| return null; | |
| }, | |
| findFirstItem(items) { | |
| for (const item of items) { | |
| if (item.link) return item; | |
| if (item.submenu) { | |
| const found = this.findFirstItem(item.submenu); | |
| if (found) return found; | |
| } | |
| } | |
| return null; | |
| }, | |
| checkHashAndLoad() { | |
| const hash = window.location.hash.slice(1); | |
| // Check if hash matches a sidebar ID first | |
| let targetItem = null; | |
| if (hash) { | |
| const path = this.findPathToId(this.sidebarMenu, hash); | |
| if (path) { | |
| path.slice(0, -1).forEach(p => { | |
| if (typeof p === 'string' && !this.openSubmenus.includes(p)) this.openSubmenus.push(p); | |
| }); | |
| targetItem = path[path.length - 1]; | |
| } | |
| } | |
| if (!targetItem) targetItem = this.findFirstItem(this.sidebarMenu); | |
| if (targetItem) this.loadPage(targetItem.link, targetItem.id); | |
| }, | |
| highlightText(text) { | |
| if (!this.searchQuery) return text; | |
| const regex = new RegExp(`(${this.searchQuery})`, 'gi'); | |
| return text.replace(regex, '<span class="text-blue-400 font-bold">$1</span>'); | |
| }, | |
| toggleSubmenu(title) { | |
| if(this.searchQuery) return; | |
| const index = this.openSubmenus.indexOf(title); | |
| if (index > -1) this.openSubmenus.splice(index, 1); | |
| else this.openSubmenus.push(title); | |
| }, | |
| async loadPage(url, id) { | |
| if (!url || url === '#') return; | |
| this.currentUrl = url; | |
| // If clicking a new page but preserving hash might be tricky, reset hash unless it's a deep link | |
| if (id && !window.location.hash.includes(id)) { | |
| // Only push state if meaningful change | |
| } | |
| this.loading = true; | |
| this.error = null; | |
| this.sidebarOpen = false; | |
| this.mobileTocOpen = false; | |
| this.toc = []; // Clear TOC while loading | |
| try { | |
| const res = await fetch(url); | |
| if (!res.ok) throw new Error(`Status: ${res.status}`); | |
| const rawMd = await res.text(); | |
| this.renderContent(rawMd); | |
| // Generate TOC after render | |
| this.$nextTick(() => { | |
| this.generateToc(); | |
| // Handle scroll to hash if deep link exists (e.g. #heading-id) | |
| const hash = window.location.hash.slice(1); | |
| if (hash) { | |
| setTimeout(() => { | |
| const el = document.getElementById(hash); | |
| if (el) el.scrollIntoView(); | |
| }, 100); | |
| } | |
| }); | |
| } catch (e) { | |
| this.error = e.message; | |
| } finally { | |
| this.loading = false; | |
| } | |
| }, | |
| renderContent(rawMd) { | |
| const processed = rawMd.replace( | |
| /(<mdiv[\s\S]*?<\/mdiv>)/gi, | |
| (match, capture) => capture.replace(/^<mdiv/, `<div class="mdiv"`).replace(/<\/mdiv>$/, '</div>') | |
| ); | |
| const el = document.getElementById("content"); | |
| if (!el) return; | |
| el.innerHTML = marked.parse(processed); | |
| el.querySelectorAll('pre').forEach(pre => { | |
| const code = pre.querySelector('code'); | |
| if (!code) return; | |
| const isMermaid = code.classList.contains('language-mermaid'); | |
| if (isMermaid) { | |
| const diagramReq = code.textContent; | |
| pre.innerHTML = ''; | |
| const mermaidDiv = document.createElement('div'); | |
| mermaidDiv.className = 'mermaid'; | |
| mermaidDiv.textContent = diagramReq; | |
| pre.appendChild(mermaidDiv); | |
| pre.style.background = 'transparent'; | |
| pre.style.border = 'none'; | |
| } else { | |
| hljs.highlightElement(code); | |
| const btn = document.createElement('button'); | |
| btn.className = 'absolute top-3 right-3 text-[10px] font-bold uppercase tracking-wider text-gray-500 hover:text-white bg-[#0d1117] border border-[#30363d] px-2 py-1 rounded opacity-0 transition-opacity z-10 cursor-pointer'; | |
| btn.innerText = 'Copy'; | |
| btn.onclick = (e) => { | |
| e.stopPropagation(); | |
| navigator.clipboard.writeText(code.innerText); | |
| btn.innerText = 'Done'; | |
| setTimeout(() => btn.innerText = 'Copy', 2000); | |
| }; | |
| pre.addEventListener('mouseenter', () => btn.classList.remove('opacity-0')); | |
| pre.addEventListener('mouseleave', () => btn.classList.add('opacity-0')); | |
| pre.appendChild(btn); | |
| } | |
| }); | |
| const diagrams = el.querySelectorAll('.mermaid'); | |
| if (diagrams.length) mermaid.run({ nodes: diagrams }); | |
| if (window.MathJax) MathJax.typesetPromise([el]); | |
| } | |
| }).mount(); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Docs</title> | |
| <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> | |
| <meta name="description" content="Documentation"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0"> | |
| <!-- Preload critical resources --> | |
| <link rel="preconnect" href="https://cdn.jsdelivr.net"> | |
| <link rel="dns-prefetch" href="https://cdn.jsdelivr.net"> | |
| <!-- Critical inline styles FIRST to prevent FOUC --> | |
| <style> | |
| /* Prevent FOUC: Set background immediately */ | |
| html, | |
| body { | |
| background-color: #0d1117; | |
| color: #c9d1d9; | |
| margin: 0; | |
| padding: 0; | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; | |
| } | |
| /* Hide app until content is injected by Docsify */ | |
| #app:empty { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| height: 100vh; | |
| width: 100vw; | |
| } | |
| #app:empty::after { | |
| content: 'Loading documentation...'; | |
| color: #8b949e; | |
| font-size: 14px; | |
| } | |
| :root { | |
| /* Base Colors - Matching GitHub Dark Dimmed */ | |
| --base-background-color: #0d1117; | |
| --base-color: #c9d1d9; | |
| --base-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; | |
| /* Sidebar */ | |
| --sidebar-background-color: #0d1117; | |
| --sidebar-border-width: 0; | |
| --sidebar-border-color: #30363d; | |
| --sidebar-nav-link-color: #8b949e; | |
| --sidebar-nav-link-color--active: #58a6ff; | |
| --sidebar-nav-link-font-weight--active: bold; | |
| --sidebar-width: 18rem; | |
| /* Content */ | |
| --content-max-width: 60em; | |
| /* Code Blocks */ | |
| --code-block-background-color: #161b22; | |
| --code-inline-background-color: #161b22; | |
| --code-inline-color: #e2e8f0; | |
| /* Links */ | |
| --link-color: #58a6ff; | |
| --link-text-decoration: none; | |
| --link-text-decoration--hover: underline; | |
| /* Header */ | |
| --heading-h2-border-style: none; | |
| /* Blockquote & Callouts */ | |
| --blockquote-border-color: #38bdf8; | |
| --blockquote-background-color: rgba(56, 189, 248, 0.1); | |
| --blockquote-color: #e2e8f0; | |
| } | |
| /* Scrollbar Styling */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #30363d; | |
| border-radius: 10px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #58a6ff; | |
| } | |
| /* Mermaid Centering */ | |
| .mermaid { | |
| display: flex; | |
| justify-content: center; | |
| background: #161b22; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| margin: 1rem 0; | |
| } | |
| </style> | |
| <!-- External stylesheets loaded AFTER critical inline styles --> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsify-themeable@0/dist/css/theme-simple-dark.css"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.css"> | |
| </head> | |
| <body> | |
| <nav style="display: flex; justify-content: space-between; align-items: center;"> | |
| <a href="/" style="color: #c9d1d9;"> | |
| <img src="" alt="Logo" style="width: 20px; height: 20px; object-fit: contain; filter: invert(1);"> | |
| Home | |
| </a> | |
| <a href="#" style="color: #c9d1d9;"> | |
| <img src="https://www.svgrepo.com/show/354420/swagger.svg" height="20"> OpenAPI | |
| </a> | |
| </nav> | |
| <div id="app"></div> | |
| <script> | |
| window.$docsify = { | |
| name: 'Docs', | |
| repo: 'https://github.com/rahuldhole/repo', | |
| homepage: 'html-custom-hidden-markdown.md', | |
| loadSidebar: true, | |
| subMaxLevel: 3, | |
| auto2top: true, | |
| basePath: '/docs/', | |
| // Search Plugin Config | |
| search: { | |
| maxAge: 86400000, | |
| paths: 'auto', | |
| placeholder: 'Search...', | |
| noData: 'No Results!', | |
| depth: 6, | |
| hideOtherSidebarContent: false, | |
| }, | |
| // Pagination Plugin | |
| pagination: { | |
| previousText: 'Previous', | |
| nextText: 'Next', | |
| crossChapter: true, | |
| }, | |
| // Copy Code Plugin | |
| copyCode: { | |
| buttonText: 'Copy', | |
| errorText: 'Error', | |
| successText: 'Copied' | |
| }, | |
| // Mermaid Config | |
| mermaidConfig: { | |
| querySelector: ".mermaid", | |
| startOnLoad: false | |
| }, | |
| // Tabs | |
| tabs: { | |
| persist: true, | |
| sync: true, | |
| theme: 'classic', | |
| tabComments: true, | |
| tabHeadings: true | |
| } | |
| } | |
| </script> | |
| <!-- Docsify v4 --> | |
| <script src="https://cdn.jsdelivr.net/npm/docsify@4"></script> | |
| <!-- Plugins --> | |
| <script src="https://cdn.jsdelivr.net/npm/docsify/lib/plugins/search.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/docsify/lib/plugins/zoom-image.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/docsify-pagination/dist/docsify-pagination.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/docsify-tabs@1/dist/docsify-tabs.min.js"></script> | |
| <!-- Mermaid Support --> | |
| <script type="module"> | |
| import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs"; | |
| mermaid.initialize({ startOnLoad: false, theme: 'dark' }); | |
| window.mermaid = mermaid; | |
| </script> | |
| <script src="https://cdn.jsdelivr.net/npm/docsify-mermaid@2/dist/docsify-mermaid.min.js"></script> | |
| <!-- Vue Support --> | |
| <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script> | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment