619c4e63f930cea165fabe87b5529a1f731af4c2
Tests/temp.md
| ... | ... | @@ -1,305 +0,0 @@ |
| 1 | -<!doctype html> |
|
| 2 | -<html lang="en"> |
|
| 3 | -<head> |
|
| 4 | - <meta charset="utf-8" /> |
|
| 5 | - <meta name="viewport" content="width=device-width,initial-scale=1" /> |
|
| 6 | - <title>Browser Git + Markdown Hierarchy (vanilla)</title> |
|
| 7 | - <link rel="manifest" href="manifest.json" /> |
|
| 8 | - <style> |
|
| 9 | - :root{--bg:#0f1720;--panel:#0b1220;--muted:#9aa4b2;--accent:#4f46e5;--card:#0b1226} |
|
| 10 | - 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} |
|
| 11 | - .app{display:grid;grid-template-columns:260px 1fr 420px;gap:12px;padding:12px;height:100vh;box-sizing:border-box} |
|
| 12 | - .panel{background:var(--panel);border-radius:8px;padding:10px;box-shadow:0 6px 18px rgba(2,6,23,.6);overflow:auto} |
|
| 13 | - header{display:flex;gap:12px;align-items:center;margin-bottom:8px} |
|
| 14 | - header h1{font-size:14px;margin:0} |
|
| 15 | - .tree{font-size:13px} |
|
| 16 | - .node{padding:6px;border-radius:6px;cursor:pointer;color:var(--muted)} |
|
| 17 | - .node:hover{background:#0f1a2b;color:#fff} |
|
| 18 | - .node.active{background:linear-gradient(90deg,var(--accent),#2dd4bf);color:#021124} |
|
| 19 | - .toolbar{display:flex;gap:6px;margin-bottom:8px} |
|
| 20 | - button{background:#0b1226;border:1px solid rgba(255,255,255,.04);color:#dbeafe;padding:6px 8px;border-radius:6px;cursor:pointer} |
|
| 21 | - 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} |
|
| 22 | - .preview{background:#071226;padding:10px;border-radius:6px;height:60vh;overflow:auto} |
|
| 23 | - .meta{display:flex;gap:8px;align-items:center;margin-bottom:8px} |
|
| 24 | - input[type=text]{padding:6px;border-radius:6px;border:1px solid rgba(255,255,255,.04);background:#071226;color:#e6eef6} |
|
| 25 | - .small{font-size:12px;color:var(--muted)} |
|
| 26 | - footer{margin-top:8px;font-size:12px;color:var(--muted)} |
|
| 27 | - </style> |
|
| 28 | -</head> |
|
| 29 | -<body> |
|
| 30 | - <div class="app"> |
|
| 31 | - <div class="panel" id="left"> |
|
| 32 | - <header> |
|
| 33 | - <h1>Repo</h1> |
|
| 34 | - <div class="small" id="status">init...</div> |
|
| 35 | - </header> |
|
| 36 | - <div class="toolbar"> |
|
| 37 | - <button id="newFile">New file</button> |
|
| 38 | - <button id="newFolder">New folder</button> |
|
| 39 | - <button id="refreshTree">Refresh</button> |
|
| 40 | - </div> |
|
| 41 | - <div class="tree" id="tree"></div> |
|
| 42 | - <footer class="small">Files stored in browser IndexedDB via LightningFS</footer> |
|
| 43 | - </div> |
|
| 44 | - |
|
| 45 | - <div class="panel" id="center"> |
|
| 46 | - <div class="meta"> |
|
| 47 | - <input id="filepath" type="text" placeholder="objects/example.md" style="flex:1" /> |
|
| 48 | - <input id="commitMsg" type="text" placeholder="Commit message" style="width:260px" /> |
|
| 49 | - <button id="saveCommit">Save & Commit</button> |
|
| 50 | - </div> |
|
| 51 | - <textarea id="editor" placeholder="File content (Markdown or YAML/JSON)"></textarea> |
|
| 52 | - <div style="display:flex;gap:8px;margin-top:8px"> |
|
| 53 | - <button id="initRepo">Init Repo</button> |
|
| 54 | - <button id="logBtn">Log</button> |
|
| 55 | - <button id="setRemote">Set Remote</button> |
|
| 56 | - <button id="pushBtn">Push</button> |
|
| 57 | - <button id="pullBtn">Pull</button> |
|
| 58 | - </div> |
|
| 59 | - <pre id="log" class="small" style="margin-top:8px;white-space:pre-wrap"></pre> |
|
| 60 | - </div> |
|
| 61 | - |
|
| 62 | - <div class="panel" id="right"> |
|
| 63 | - <header><h1>Markdown Preview</h1></header> |
|
| 64 | - <div class="preview" id="preview">Nothing selected</div> |
|
| 65 | - </div> |
|
| 66 | - </div> |
|
| 67 | - |
|
| 68 | - <script type="module"> |
|
| 69 | - import LightningFS from 'https://unpkg.com/@isomorphic-git/lightning-fs@4.0.1?module'; |
|
| 70 | - import * as git from 'https://unpkg.com/isomorphic-git@1.20.0?module'; |
|
| 71 | - import { marked } from 'https://cdn.jsdelivr.net/npm/marked@5.1.1/+esm'; |
|
| 72 | - |
|
| 73 | - // Basic globals |
|
| 74 | - const repoDir = '/repo'; |
|
| 75 | - const fs = new LightningFS('browser-fs', { wipe: false }); |
|
| 76 | - git.plugins.set('fs', fs); |
|
| 77 | - |
|
| 78 | - const pfs = fs.promises; |
|
| 79 | - const treeEl = document.getElementById('tree'); |
|
| 80 | - const editor = document.getElementById('editor'); |
|
| 81 | - const preview = document.getElementById('preview'); |
|
| 82 | - const filepathInput = document.getElementById('filepath'); |
|
| 83 | - const commitMsgInput = document.getElementById('commitMsg'); |
|
| 84 | - const statusEl = document.getElementById('status'); |
|
| 85 | - const logEl = document.getElementById('log'); |
|
| 86 | - |
|
| 87 | - // Utility: ensure repo dir exists |
|
| 88 | - async function ensureRepoDir() { |
|
| 89 | - try { await pfs.stat(repoDir); } catch (e) { await pfs.mkdir(repoDir); } |
|
| 90 | - } |
|
| 91 | - |
|
| 92 | - // Initialize git repo if not present |
|
| 93 | - async function initRepo() { |
|
| 94 | - status('initializing repo...'); |
|
| 95 | - await ensureRepoDir(); |
|
| 96 | - try { |
|
| 97 | - const isRepo = await git.isRepo({ fs, dir: repoDir }); |
|
| 98 | - if (!isRepo) { |
|
| 99 | - await git.init({ fs, dir: repoDir, defaultBranch: 'main' }); |
|
| 100 | - status('repo initialized'); |
|
| 101 | - } else { |
|
| 102 | - status('repo already initialized'); |
|
| 103 | - } |
|
| 104 | - } catch (e) { |
|
| 105 | - console.error(e); |
|
| 106 | - status('init error: ' + e.message); |
|
| 107 | - } |
|
| 108 | - await refreshTree(); |
|
| 109 | - } |
|
| 110 | - |
|
| 111 | - // Read directory recursively and build tree |
|
| 112 | - async function readTree(dir = repoDir) { |
|
| 113 | - const items = []; |
|
| 114 | - try { |
|
| 115 | - const names = await pfs.readdir(dir); |
|
| 116 | - for (const name of names) { |
|
| 117 | - const path = dir + '/' + name; |
|
| 118 | - const st = await pfs.stat(path); |
|
| 119 | - if (st && st.type === 'directory') { |
|
| 120 | - items.push({ name, path, type: 'dir', children: await readTree(path) }); |
|
| 121 | - } else { |
|
| 122 | - items.push({ name, path, type: 'file' }); |
|
| 123 | - } |
|
| 124 | - } |
|
| 125 | - } catch (e) { |
|
| 126 | - // ignore |
|
| 127 | - } |
|
| 128 | - return items; |
|
| 129 | - } |
|
| 130 | - |
|
| 131 | - function renderTree(nodes, container) { |
|
| 132 | - container.innerHTML = ''; |
|
| 133 | - for (const node of nodes) { |
|
| 134 | - const div = document.createElement('div'); |
|
| 135 | - div.className = 'node'; |
|
| 136 | - div.textContent = node.name; |
|
| 137 | - div.title = node.path; |
|
| 138 | - div.onclick = async (ev) => { |
|
| 139 | - ev.stopPropagation(); |
|
| 140 | - document.querySelectorAll('.node').forEach(n=>n.classList.remove('active')); |
|
| 141 | - div.classList.add('active'); |
|
| 142 | - if (node.type === 'file') { |
|
| 143 | - const rel = node.path.replace(repoDir + '/', ''); |
|
| 144 | - filepathInput.value = rel; |
|
| 145 | - const data = await pfs.readFile(node.path, { encoding: 'utf8' }).catch(()=>''); |
|
| 146 | - editor.value = data; |
|
| 147 | - preview.innerHTML = marked.parse(data || ''); |
|
| 148 | - } else { |
|
| 149 | - // toggle expand: show children inline |
|
| 150 | - if (div.nextSibling && div.nextSibling.classList && div.nextSibling.classList.contains('children')) { |
|
| 151 | - div.nextSibling.remove(); |
|
| 152 | - } else { |
|
| 153 | - const childWrap = document.createElement('div'); |
|
| 154 | - childWrap.className = 'children'; |
|
| 155 | - childWrap.style.paddingLeft = '12px'; |
|
| 156 | - renderTree(node.children, childWrap); |
|
| 157 | - div.after(childWrap); |
|
| 158 | - } |
|
| 159 | - } |
|
| 160 | - }; |
|
| 161 | - container.appendChild(div); |
|
| 162 | - } |
|
| 163 | - } |
|
| 164 | - |
|
| 165 | - async function refreshTree() { |
|
| 166 | - const nodes = await readTree(); |
|
| 167 | - renderTree(nodes, treeEl); |
|
| 168 | - status('tree refreshed'); |
|
| 169 | - } |
|
| 170 | - |
|
| 171 | - // Save file to fs and stage/commit |
|
| 172 | - async function saveAndCommit() { |
|
| 173 | - const rel = filepathInput.value.trim(); |
|
| 174 | - if (!rel) { alert('Enter a filepath'); return; } |
|
| 175 | - const full = repoDir + '/' + rel; |
|
| 176 | - // ensure parent dirs |
|
| 177 | - const parts = full.split('/'); |
|
| 178 | - for (let i = 2; i <= parts.length - 1; i++) { |
|
| 179 | - const p = parts.slice(0, i).join('/'); |
|
| 180 | - try { await pfs.stat(p); } catch (e) { await pfs.mkdir(p); } |
|
| 181 | - } |
|
| 182 | - await pfs.writeFile(full, editor.value, { encoding: 'utf8' }); |
|
| 183 | - await git.add({ fs, dir: repoDir, filepath: rel }); |
|
| 184 | - const message = commitMsgInput.value.trim() || 'Edit ' + rel; |
|
| 185 | - try { |
|
| 186 | - const sha = await git.commit({ |
|
| 187 | - fs, dir: repoDir, |
|
| 188 | - author: { name: 'Browser User', email: 'user@local' }, |
|
| 189 | - message |
|
| 190 | - }); |
|
| 191 | - status('committed ' + sha.slice(0,7)); |
|
| 192 | - await refreshTree(); |
|
| 193 | - log('Committed: ' + sha); |
|
| 194 | - } catch (e) { |
|
| 195 | - console.error(e); |
|
| 196 | - status('commit error: ' + e.message); |
|
| 197 | - } |
|
| 198 | - } |
|
| 199 | - |
|
| 200 | - // Show log |
|
| 201 | - async function showLog() { |
|
| 202 | - try { |
|
| 203 | - const commits = await git.log({ fs, dir: repoDir, depth: 20 }); |
|
| 204 | - logEl.textContent = commits.map(c => `${c.oid.slice(0,7)} ${c.commit.message} — ${c.commit.author.name}`).join('\n\n'); |
|
| 205 | - } catch (e) { |
|
| 206 | - logEl.textContent = 'Log error: ' + e.message; |
|
| 207 | - } |
|
| 208 | - } |
|
| 209 | - |
|
| 210 | - // Set remote (store in localStorage) |
|
| 211 | - function setRemote() { |
|
| 212 | - const url = prompt('Remote URL (HTTPS, e.g. https://github.com/you/repo.git)'); |
|
| 213 | - if (url) { |
|
| 214 | - localStorage.setItem('remoteUrl', url); |
|
| 215 | - status('remote set'); |
|
| 216 | - } |
|
| 217 | - } |
|
| 218 | - |
|
| 219 | - // Push (prompts for token) |
|
| 220 | - async function push() { |
|
| 221 | - const remote = localStorage.getItem('remoteUrl'); |
|
| 222 | - if (!remote) { alert('Set remote first'); return; } |
|
| 223 | - const token = prompt('Personal access token (will not be stored). For GitHub use token as password and username "x-access-token" or "token".'); |
|
| 224 | - if (!token) return; |
|
| 225 | - status('pushing...'); |
|
| 226 | - try { |
|
| 227 | - await git.addRemote({ fs, dir: repoDir, remote: 'origin', url: remote }).catch(()=>{}); |
|
| 228 | - await git.push({ |
|
| 229 | - fs, http: git.http, dir: repoDir, |
|
| 230 | - remote: 'origin', ref: 'main', |
|
| 231 | - onAuth: () => ({ username: 'x-access-token', password: token }) |
|
| 232 | - }); |
|
| 233 | - status('push complete'); |
|
| 234 | - } catch (e) { |
|
| 235 | - console.error(e); |
|
| 236 | - status('push failed: ' + e.message); |
|
| 237 | - } |
|
| 238 | - } |
|
| 239 | - |
|
| 240 | - // Pull (prompts for token) |
|
| 241 | - async function pull() { |
|
| 242 | - const remote = localStorage.getItem('remoteUrl'); |
|
| 243 | - if (!remote) { alert('Set remote first'); return; } |
|
| 244 | - const token = prompt('Personal access token (will not be stored).'); |
|
| 245 | - if (!token) return; |
|
| 246 | - status('pulling...'); |
|
| 247 | - try { |
|
| 248 | - await git.addRemote({ fs, dir: repoDir, remote: 'origin', url: remote }).catch(()=>{}); |
|
| 249 | - await git.pull({ |
|
| 250 | - fs, http: git.http, dir: repoDir, |
|
| 251 | - remote: 'origin', ref: 'main', |
|
| 252 | - singleBranch: true, |
|
| 253 | - author: { name: 'Browser User', email: 'user@local' }, |
|
| 254 | - onAuth: () => ({ username: 'x-access-token', password: token }) |
|
| 255 | - }); |
|
| 256 | - status('pull complete'); |
|
| 257 | - await refreshTree(); |
|
| 258 | - } catch (e) { |
|
| 259 | - console.error(e); |
|
| 260 | - status('pull failed: ' + e.message); |
|
| 261 | - } |
|
| 262 | - } |
|
| 263 | - |
|
| 264 | - // Helpers |
|
| 265 | - function status(text) { statusEl.textContent = text; } |
|
| 266 | - function log(text) { logEl.textContent = (new Date()).toLocaleTimeString() + ' — ' + text + '\n' + logEl.textContent; } |
|
| 267 | - |
|
| 268 | - // Wire UI |
|
| 269 | - document.getElementById('initRepo').onclick = initRepo; |
|
| 270 | - document.getElementById('refreshTree').onclick = refreshTree; |
|
| 271 | - document.getElementById('saveCommit').onclick = saveAndCommit; |
|
| 272 | - document.getElementById('logBtn').onclick = showLog; |
|
| 273 | - document.getElementById('newFile').onclick = async () => { |
|
| 274 | - const name = prompt('New file path (e.g. objects/note.md)'); |
|
| 275 | - if (!name) return; |
|
| 276 | - filepathInput.value = name; |
|
| 277 | - editor.value = '# ' + name.split('/').pop(); |
|
| 278 | - }; |
|
| 279 | - document.getElementById('newFolder').onclick = async () => { |
|
| 280 | - const name = prompt('New folder path (e.g. objects/notes)'); |
|
| 281 | - if (!name) return; |
|
| 282 | - const full = repoDir + '/' + name; |
|
| 283 | - try { await pfs.mkdir(full, { recursive: true }); status('folder created'); await refreshTree(); } catch(e){status('mkdir error')} |
|
| 284 | - }; |
|
| 285 | - document.getElementById('setRemote').onclick = setRemote; |
|
| 286 | - document.getElementById('pushBtn').onclick = push; |
|
| 287 | - document.getElementById('pullBtn').onclick = pull; |
|
| 288 | - |
|
| 289 | - // Live preview |
|
| 290 | - editor.addEventListener('input', () => { |
|
| 291 | - preview.innerHTML = marked.parse(editor.value || ''); |
|
| 292 | - }); |
|
| 293 | - |
|
| 294 | - // Register service worker |
|
| 295 | - if ('serviceWorker' in navigator) { |
|
| 296 | - navigator.serviceWorker.register('sw.js').then(()=>console.log('sw registered')).catch(()=>console.warn('sw failed')); |
|
| 297 | - } |
|
| 298 | - |
|
| 299 | - // Auto init on load |
|
| 300 | - await initRepo(); |
|
| 301 | - await refreshTree(); |
|
| 302 | - status('ready'); |
|
| 303 | - </script> |
|
| 304 | -</body> |
|
| 305 | -</html> |