☰
Current Page
Main Menu
Home
Home
Editing temp
Edit
Preview
H1
H2
H3
default
Set your preferred keybinding
default
vim
emacs
txt
Set this page's format to
Markdown
Plain Text
Pod
RDoc
reStructuredText
Rendering unavailable for
AsciiDoc
BibTeX
Creole
MediaWiki
Org-mode
Textile
Help 1
Help 1
Help 1
Help 2
Help 3
Help 4
Help 5
Help 6
Help 7
Help 8
Autosaved text is available. Click the button to restore it.
Restore Text
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <title>Browser Git + Markdown Hierarchy (vanilla)</title> <link rel="manifest" href="manifest.json" /> <style> :root{--bg:#0f1720;--panel:#0b1220;--muted:#9aa4b2;--accent:#4f46e5;--card:#0b1226} html,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial;background:linear-gradient(180deg,#071025 0%,#071827 100%);color:#e6eef6} .app{display:grid;grid-template-columns:260px 1fr 420px;gap:12px;padding:12px;height:100vh;box-sizing:border-box} .panel{background:var(--panel);border-radius:8px;padding:10px;box-shadow:0 6px 18px rgba(2,6,23,.6);overflow:auto} header{display:flex;gap:12px;align-items:center;margin-bottom:8px} header h1{font-size:14px;margin:0} .tree{font-size:13px} .node{padding:6px;border-radius:6px;cursor:pointer;color:var(--muted)} .node:hover{background:#0f1a2b;color:#fff} .node.active{background:linear-gradient(90deg,var(--accent),#2dd4bf);color:#021124} .toolbar{display:flex;gap:6px;margin-bottom:8px} button{background:#0b1226;border:1px solid rgba(255,255,255,.04);color:#dbeafe;padding:6px 8px;border-radius:6px;cursor:pointer} textarea{width:100%;height:60vh;background:#071226;color:#e6eef6;border:1px solid rgba(255,255,255,.04);padding:8px;border-radius:6px;font-family:monospace;font-size:13px} .preview{background:#071226;padding:10px;border-radius:6px;height:60vh;overflow:auto} .meta{display:flex;gap:8px;align-items:center;margin-bottom:8px} input[type=text]{padding:6px;border-radius:6px;border:1px solid rgba(255,255,255,.04);background:#071226;color:#e6eef6} .small{font-size:12px;color:var(--muted)} footer{margin-top:8px;font-size:12px;color:var(--muted)} </style> </head> <body> <div class="app"> <div class="panel" id="left"> <header> <h1>Repo</h1> <div class="small" id="status">init...</div> </header> <div class="toolbar"> <button id="newFile">New file</button> <button id="newFolder">New folder</button> <button id="refreshTree">Refresh</button> </div> <div class="tree" id="tree"></div> <footer class="small">Files stored in browser IndexedDB via LightningFS</footer> </div> <div class="panel" id="center"> <div class="meta"> <input id="filepath" type="text" placeholder="objects/example.md" style="flex:1" /> <input id="commitMsg" type="text" placeholder="Commit message" style="width:260px" /> <button id="saveCommit">Save & Commit</button> </div> <textarea id="editor" placeholder="File content (Markdown or YAML/JSON)"></textarea> <div style="display:flex;gap:8px;margin-top:8px"> <button id="initRepo">Init Repo</button> <button id="logBtn">Log</button> <button id="setRemote">Set Remote</button> <button id="pushBtn">Push</button> <button id="pullBtn">Pull</button> </div> <pre id="log" class="small" style="margin-top:8px;white-space:pre-wrap"></pre> </div> <div class="panel" id="right"> <header><h1>Markdown Preview</h1></header> <div class="preview" id="preview">Nothing selected</div> </div> </div> <script type="module"> import LightningFS from 'https://unpkg.com/@isomorphic-git/lightning-fs@4.0.1?module'; import * as git from 'https://unpkg.com/isomorphic-git@1.20.0?module'; import { marked } from 'https://cdn.jsdelivr.net/npm/marked@5.1.1/+esm'; // Basic globals const repoDir = '/repo'; const fs = new LightningFS('browser-fs', { wipe: false }); git.plugins.set('fs', fs); const pfs = fs.promises; const treeEl = document.getElementById('tree'); const editor = document.getElementById('editor'); const preview = document.getElementById('preview'); const filepathInput = document.getElementById('filepath'); const commitMsgInput = document.getElementById('commitMsg'); const statusEl = document.getElementById('status'); const logEl = document.getElementById('log'); // Utility: ensure repo dir exists async function ensureRepoDir() { try { await pfs.stat(repoDir); } catch (e) { await pfs.mkdir(repoDir); } } // Initialize git repo if not present async function initRepo() { status('initializing repo...'); await ensureRepoDir(); try { const isRepo = await git.isRepo({ fs, dir: repoDir }); if (!isRepo) { await git.init({ fs, dir: repoDir, defaultBranch: 'main' }); status('repo initialized'); } else { status('repo already initialized'); } } catch (e) { console.error(e); status('init error: ' + e.message); } await refreshTree(); } // Read directory recursively and build tree async function readTree(dir = repoDir) { const items = []; try { const names = await pfs.readdir(dir); for (const name of names) { const path = dir + '/' + name; const st = await pfs.stat(path); if (st && st.type === 'directory') { items.push({ name, path, type: 'dir', children: await readTree(path) }); } else { items.push({ name, path, type: 'file' }); } } } catch (e) { // ignore } return items; } function renderTree(nodes, container) { container.innerHTML = ''; for (const node of nodes) { const div = document.createElement('div'); div.className = 'node'; div.textContent = node.name; div.title = node.path; div.onclick = async (ev) => { ev.stopPropagation(); document.querySelectorAll('.node').forEach(n=>n.classList.remove('active')); div.classList.add('active'); if (node.type === 'file') { const rel = node.path.replace(repoDir + '/', ''); filepathInput.value = rel; const data = await pfs.readFile(node.path, { encoding: 'utf8' }).catch(()=>''); editor.value = data; preview.innerHTML = marked.parse(data || ''); } else { // toggle expand: show children inline if (div.nextSibling && div.nextSibling.classList && div.nextSibling.classList.contains('children')) { div.nextSibling.remove(); } else { const childWrap = document.createElement('div'); childWrap.className = 'children'; childWrap.style.paddingLeft = '12px'; renderTree(node.children, childWrap); div.after(childWrap); } } }; container.appendChild(div); } } async function refreshTree() { const nodes = await readTree(); renderTree(nodes, treeEl); status('tree refreshed'); } // Save file to fs and stage/commit async function saveAndCommit() { const rel = filepathInput.value.trim(); if (!rel) { alert('Enter a filepath'); return; } const full = repoDir + '/' + rel; // ensure parent dirs const parts = full.split('/'); for (let i = 2; i <= parts.length - 1; i++) { const p = parts.slice(0, i).join('/'); try { await pfs.stat(p); } catch (e) { await pfs.mkdir(p); } } await pfs.writeFile(full, editor.value, { encoding: 'utf8' }); await git.add({ fs, dir: repoDir, filepath: rel }); const message = commitMsgInput.value.trim() || 'Edit ' + rel; try { const sha = await git.commit({ fs, dir: repoDir, author: { name: 'Browser User', email: 'user@local' }, message }); status('committed ' + sha.slice(0,7)); await refreshTree(); log('Committed: ' + sha); } catch (e) { console.error(e); status('commit error: ' + e.message); } } // Show log async function showLog() { try { const commits = await git.log({ fs, dir: repoDir, depth: 20 }); logEl.textContent = commits.map(c => `${c.oid.slice(0,7)} ${c.commit.message} — ${c.commit.author.name}`).join('\n\n'); } catch (e) { logEl.textContent = 'Log error: ' + e.message; } } // Set remote (store in localStorage) function setRemote() { const url = prompt('Remote URL (HTTPS, e.g. https://github.com/you/repo.git)'); if (url) { localStorage.setItem('remoteUrl', url); status('remote set'); } } // Push (prompts for token) async function push() { const remote = localStorage.getItem('remoteUrl'); if (!remote) { alert('Set remote first'); return; } const token = prompt('Personal access token (will not be stored). For GitHub use token as password and username "x-access-token" or "token".'); if (!token) return; status('pushing...'); try { await git.addRemote({ fs, dir: repoDir, remote: 'origin', url: remote }).catch(()=>{}); await git.push({ fs, http: git.http, dir: repoDir, remote: 'origin', ref: 'main', onAuth: () => ({ username: 'x-access-token', password: token }) }); status('push complete'); } catch (e) { console.error(e); status('push failed: ' + e.message); } } // Pull (prompts for token) async function pull() { const remote = localStorage.getItem('remoteUrl'); if (!remote) { alert('Set remote first'); return; } const token = prompt('Personal access token (will not be stored).'); if (!token) return; status('pulling...'); try { await git.addRemote({ fs, dir: repoDir, remote: 'origin', url: remote }).catch(()=>{}); await git.pull({ fs, http: git.http, dir: repoDir, remote: 'origin', ref: 'main', singleBranch: true, author: { name: 'Browser User', email: 'user@local' }, onAuth: () => ({ username: 'x-access-token', password: token }) }); status('pull complete'); await refreshTree(); } catch (e) { console.error(e); status('pull failed: ' + e.message); } } // Helpers function status(text) { statusEl.textContent = text; } function log(text) { logEl.textContent = (new Date()).toLocaleTimeString() + ' — ' + text + '\n' + logEl.textContent; } // Wire UI document.getElementById('initRepo').onclick = initRepo; document.getElementById('refreshTree').onclick = refreshTree; document.getElementById('saveCommit').onclick = saveAndCommit; document.getElementById('logBtn').onclick = showLog; document.getElementById('newFile').onclick = async () => { const name = prompt('New file path (e.g. objects/note.md)'); if (!name) return; filepathInput.value = name; editor.value = '# ' + name.split('/').pop(); }; document.getElementById('newFolder').onclick = async () => { const name = prompt('New folder path (e.g. objects/notes)'); if (!name) return; const full = repoDir + '/' + name; try { await pfs.mkdir(full, { recursive: true }); status('folder created'); await refreshTree(); } catch(e){status('mkdir error')} }; document.getElementById('setRemote').onclick = setRemote; document.getElementById('pushBtn').onclick = push; document.getElementById('pullBtn').onclick = pull; // Live preview editor.addEventListener('input', () => { preview.innerHTML = marked.parse(editor.value || ''); }); // Register service worker if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js').then(()=>console.log('sw registered')).catch(()=>console.warn('sw failed')); } // Auto init on load await initRepo(); await refreshTree(); status('ready'); </script> </body> </html>
Uploading file...
Edit message:
Cancel