Last active
October 30, 2025 21:18
-
-
Save thimslugga/2fc75b11c9d9776b71fc3743e875bc68 to your computer and use it in GitHub Desktop.
Simple wiki that renders markdown notes
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"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>WikiMD</title> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; | |
| display: flex; | |
| height: 100vh; | |
| overflow: hidden; | |
| background: #f5f5f5; | |
| } | |
| /* Sidebar */ | |
| #sidebar { | |
| width: 280px; | |
| background: #2c3e50; | |
| color: #ecf0f1; | |
| display: flex; | |
| flex-direction: column; | |
| border-right: 1px solid #34495e; | |
| } | |
| #sidebar-header { | |
| padding: 20px; | |
| background: #34495e; | |
| border-bottom: 1px solid #2c3e50; | |
| } | |
| #sidebar-header h1 { | |
| font-size: 20px; | |
| margin-bottom: 10px; | |
| } | |
| #search-box { | |
| width: 100%; | |
| padding: 8px 12px; | |
| border: none; | |
| border-radius: 4px; | |
| background: #2c3e50; | |
| color: #ecf0f1; | |
| font-size: 14px; | |
| } | |
| #search-box::placeholder { | |
| color: #95a5a6; | |
| } | |
| #file-list { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 10px 0; | |
| } | |
| .file-item { | |
| padding: 12px 20px; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| border-left: 3px solid transparent; | |
| font-size: 14px; | |
| } | |
| .file-item:hover { | |
| background: #34495e; | |
| } | |
| .file-item.active { | |
| background: #34495e; | |
| border-left-color: #3498db; | |
| } | |
| .file-item-name { | |
| font-weight: 500; | |
| } | |
| .file-item-path { | |
| font-size: 11px; | |
| color: #95a5a6; | |
| margin-top: 2px; | |
| } | |
| /* Main content */ | |
| #main-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| background: white; | |
| } | |
| #toolbar { | |
| padding: 15px 30px; | |
| background: white; | |
| border-bottom: 1px solid #e1e4e8; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| #current-file { | |
| font-size: 18px; | |
| font-weight: 600; | |
| color: #2c3e50; | |
| } | |
| .btn { | |
| padding: 8px 16px; | |
| border: none; | |
| border-radius: 4px; | |
| background: #3498db; | |
| color: white; | |
| cursor: pointer; | |
| font-size: 14px; | |
| transition: background 0.2s; | |
| } | |
| .btn:hover { | |
| background: #2980b9; | |
| } | |
| .btn-secondary { | |
| background: #95a5a6; | |
| } | |
| .btn-secondary:hover { | |
| background: #7f8c8d; | |
| } | |
| #content-area { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 40px; | |
| } | |
| /* Markdown styles */ | |
| #rendered-content { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| line-height: 1.6; | |
| color: #333; | |
| } | |
| #rendered-content h1 { | |
| font-size: 2em; | |
| margin: 0.67em 0; | |
| border-bottom: 1px solid #e1e4e8; | |
| padding-bottom: 0.3em; | |
| } | |
| #rendered-content h2 { | |
| font-size: 1.5em; | |
| margin: 0.75em 0; | |
| border-bottom: 1px solid #e1e4e8; | |
| padding-bottom: 0.3em; | |
| } | |
| #rendered-content h3 { | |
| font-size: 1.25em; | |
| margin: 1em 0; | |
| } | |
| #rendered-content p { | |
| margin: 1em 0; | |
| } | |
| #rendered-content code { | |
| background: #f6f8fa; | |
| padding: 2px 6px; | |
| border-radius: 3px; | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.9em; | |
| } | |
| #rendered-content pre { | |
| background: #f6f8fa; | |
| padding: 16px; | |
| border-radius: 6px; | |
| overflow-x: auto; | |
| margin: 1em 0; | |
| } | |
| #rendered-content pre code { | |
| background: none; | |
| padding: 0; | |
| } | |
| #rendered-content ul, #rendered-content ol { | |
| margin: 1em 0; | |
| padding-left: 2em; | |
| } | |
| #rendered-content li { | |
| margin: 0.5em 0; | |
| } | |
| #rendered-content blockquote { | |
| border-left: 4px solid #dfe2e5; | |
| padding-left: 1em; | |
| color: #6a737d; | |
| margin: 1em 0; | |
| } | |
| #rendered-content a { | |
| color: #3498db; | |
| text-decoration: none; | |
| } | |
| #rendered-content a:hover { | |
| text-decoration: underline; | |
| } | |
| #rendered-content table { | |
| border-collapse: collapse; | |
| width: 100%; | |
| margin: 1em 0; | |
| } | |
| #rendered-content th, #rendered-content td { | |
| border: 1px solid #dfe2e5; | |
| padding: 8px 12px; | |
| text-align: left; | |
| } | |
| #rendered-content th { | |
| background: #f6f8fa; | |
| font-weight: 600; | |
| } | |
| #rendered-content img { | |
| max-width: 100%; | |
| height: auto; | |
| } | |
| /* Welcome screen */ | |
| #welcome-screen { | |
| text-align: center; | |
| color: #7f8c8d; | |
| } | |
| #welcome-screen h2 { | |
| font-size: 24px; | |
| margin-bottom: 20px; | |
| } | |
| #file-input-wrapper { | |
| margin-top: 20px; | |
| } | |
| /* Scrollbar styling */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #f1f1f1; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #888; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #555; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Sidebar --> | |
| <div id="sidebar"> | |
| <div id="sidebar-header"> | |
| <h1>📚 Markdown Wiki</h1> | |
| <input type="text" id="search-box" placeholder="Search notes..."> | |
| </div> | |
| <div id="file-list"></div> | |
| </div> | |
| <!-- Main content --> | |
| <div id="main-content"> | |
| <div id="toolbar"> | |
| <div id="current-file">Welcome</div> | |
| <div> | |
| <button class="btn" onclick="loadFiles()">Load Notes</button> | |
| <button class="btn btn-secondary" onclick="clearAll()">Clear All</button> | |
| </div> | |
| </div> | |
| <div id="content-area"> | |
| <div id="welcome-screen"> | |
| <h2>WikiMD</h2> | |
| <p>Load your markdown files to get started</p> | |
| <div id="file-input-wrapper"> | |
| <input type="file" id="file-input" multiple accept=".md,.markdown,.txt" style="display:none"> | |
| <button class="btn" onclick="document.getElementById('file-input').click()"> | |
| Choose Files | |
| </button> | |
| </div> | |
| </div> | |
| <div id="rendered-content"></div> | |
| </div> | |
| </div> | |
| <script> | |
| // Store loaded files | |
| let notes = []; | |
| let currentNote = null; | |
| // Initialize marked | |
| marked.setOptions({ | |
| breaks: true, | |
| gfm: true | |
| }); | |
| // File input handler | |
| document.getElementById('file-input').addEventListener('change', async (e) => { | |
| const files = Array.from(e.target.files); | |
| await loadFilesFromInput(files); | |
| }); | |
| async function loadFilesFromInput(files) { | |
| notes = []; | |
| for (const file of files) { | |
| const content = await file.text(); | |
| notes.push({ | |
| name: file.name, | |
| path: file.webkitRelativePath || file.name, | |
| content: content | |
| }); | |
| } | |
| renderFileList(); | |
| if (notes.length > 0) { | |
| document.getElementById('welcome-screen').style.display = 'none'; | |
| selectNote(notes[0]); | |
| } | |
| } | |
| function renderFileList() { | |
| const fileList = document.getElementById('file-list'); | |
| const searchTerm = document.getElementById('search-box').value.toLowerCase(); | |
| const filteredNotes = notes.filter(note => | |
| note.name.toLowerCase().includes(searchTerm) || | |
| note.content.toLowerCase().includes(searchTerm) | |
| ); | |
| fileList.innerHTML = filteredNotes.map((note, index) => ` | |
| <div class="file-item ${currentNote === note ? 'active' : ''}" | |
| onclick="selectNote(notes[${notes.indexOf(note)}])"> | |
| <div class="file-item-name">${note.name}</div> | |
| <div class="file-item-path">${note.path}</div> | |
| </div> | |
| `).join(''); | |
| } | |
| function selectNote(note) { | |
| currentNote = note; | |
| document.getElementById('current-file').textContent = note.name; | |
| const renderedContent = document.getElementById('rendered-content'); | |
| renderedContent.innerHTML = marked.parse(note.content); | |
| renderedContent.style.display = 'block'; | |
| renderFileList(); | |
| } | |
| function loadFiles() { | |
| document.getElementById('file-input').click(); | |
| } | |
| function clearAll() { | |
| notes = []; | |
| currentNote = null; | |
| document.getElementById('file-list').innerHTML = ''; | |
| document.getElementById('rendered-content').innerHTML = ''; | |
| document.getElementById('rendered-content').style.display = 'none'; | |
| document.getElementById('welcome-screen').style.display = 'block'; | |
| document.getElementById('current-file').textContent = 'Welcome'; | |
| } | |
| // Search functionality | |
| document.getElementById('search-box').addEventListener('input', () => { | |
| renderFileList(); | |
| }); | |
| // Handle wiki-style links [[Note Name]] | |
| document.addEventListener('click', (e) => { | |
| if (e.target.tagName === 'A') { | |
| const href = e.target.getAttribute('href'); | |
| if (href && href.startsWith('[[') && href.endsWith(']]')) { | |
| e.preventDefault(); | |
| const noteName = href.slice(2, -2); | |
| const note = notes.find(n => | |
| n.name === noteName || | |
| n.name === noteName + '.md' | |
| ); | |
| if (note) { | |
| selectNote(note); | |
| } | |
| } | |
| } | |
| }); | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| // Ctrl/Cmd + K for search | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
| e.preventDefault(); | |
| document.getElementById('search-box').focus(); | |
| } | |
| // Arrow keys for navigation | |
| if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { | |
| const currentIndex = notes.indexOf(currentNote); | |
| if (currentIndex !== -1) { | |
| e.preventDefault(); | |
| const newIndex = e.key === 'ArrowDown' | |
| ? Math.min(currentIndex + 1, notes.length - 1) | |
| : Math.max(currentIndex - 1, 0); | |
| selectNote(notes[newIndex]); | |
| } | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment