| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>GH-Hub</title>
- <style>
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
- :root {
- --bg: var(--vscode-editor-background, #1e1e1e);
- --bg2: var(--vscode-sideBar-background, #252526);
- --bg3: var(--vscode-input-background, #3c3c3c);
- --border: var(--vscode-panel-border, #474747);
- --text: var(--vscode-editor-foreground, #cccccc);
- --text2: var(--vscode-descriptionForeground, #858585);
- --accent: var(--vscode-textLink-foreground, #3794ff);
- --green: #3fb950; --yellow: #d29922; --red: #f85149; --purple: #a371f7;
- --font: var(--vscode-font-family, system-ui);
- --mono: var(--vscode-editor-font-family, monospace);
- --radius: 4px;
- }
- body { font-family: var(--font); background: var(--bg); color: var(--text); font-size: 13px; line-height: 1.5; height: 100vh; display: flex; flex-direction: column; }
- a { color: var(--accent); text-decoration: none; }
- a:hover { text-decoration: underline; }
- button {
- font-family: var(--font); font-size: 12px; padding: 4px 12px;
- border: 1px solid var(--border); border-radius: var(--radius);
- background: var(--bg3); color: var(--text); cursor: pointer;
- }
- button:hover { opacity: 0.85; }
- button.primary { background: #238636; border-color: #2ea043; color: #fff; }
- button.danger { background: #da3633; border-color: #f85149; color: #fff; }
- button.ai { background: #1f1a3e; border-color: var(--purple); color: var(--purple); }
- button:disabled { opacity: 0.4; cursor: default; }
- input, textarea, select {
- font-family: var(--font); font-size: 12px; padding: 4px 8px;
- border: 1px solid var(--border); border-radius: var(--radius);
- background: var(--bg3); color: var(--text); width: 100%;
- }
- textarea { resize: vertical; min-height: 80px; font-family: var(--mono); font-size: 12px; }
- /* ─── Header ──────────────────────────────── */
- .header {
- display: flex; align-items: center; gap: 10px; padding: 8px 14px;
- background: var(--bg2); border-bottom: 1px solid var(--border);
- }
- .header h1 { font-size: 15px; font-weight: 600; }
- .header h1 b { color: var(--accent); }
- .header .user { color: var(--text2); font-size: 12px; }
- .header .spacer { flex: 1; }
- .badge {
- background: var(--accent); color: #000; font-size: 10px; font-weight: 700;
- padding: 1px 7px; border-radius: 8px; cursor: pointer; min-width: 18px; text-align: center;
- }
- .badge.zero { background: var(--bg3); color: var(--text2); }
- .dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
- .dot.ok { background: var(--green); }
- .dot.err { background: var(--red); }
- /* ─── Tabs ────────────────────────────────── */
- .tabs {
- display: flex; background: var(--bg2); border-bottom: 1px solid var(--border); padding: 0 14px;
- }
- .tab {
- padding: 7px 14px; font-size: 12px; color: var(--text2); cursor: pointer;
- border-bottom: 2px solid transparent;
- }
- .tab:hover { color: var(--text); }
- .tab.active { color: var(--text); border-bottom-color: var(--accent); }
- /* ─── Panels ──────────────────────────────── */
- .content { flex: 1; overflow: hidden; }
- .panel { display: none; height: 100%; overflow-y: auto; padding: 14px; }
- .panel.active { display: flex; flex-direction: column; }
- /* ─── Repos ───────────────────────────────── */
- .toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; }
- .toolbar h2 { font-size: 14px; flex: 1; }
- .repo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 8px; flex: 1; overflow-y: auto; }
- .repo-card {
- background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
- padding: 12px; cursor: pointer;
- }
- .repo-card:hover { border-color: var(--accent); }
- .repo-card.sel { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
- .repo-card h3 { font-size: 13px; color: var(--accent); }
- .repo-card .desc { color: var(--text2); font-size: 11px; margin: 2px 0 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
- .repo-meta { display: flex; gap: 12px; font-size: 11px; color: var(--text2); }
- .lang-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 2px; vertical-align: middle; }
- .tag-private { color: var(--yellow); border: 1px solid var(--yellow); padding: 0 4px; border-radius: 3px; font-size: 9px; }
- /* ─── Split View ──────────────────────────── */
- .split { display: flex; height: 100%; gap: 0; flex: 1; overflow: hidden; }
- .list-pane { width: 320px; min-width: 240px; border-right: 1px solid var(--border); overflow-y: auto; display: flex; flex-direction: column; }
- .detail-pane { flex: 1; overflow-y: auto; padding: 14px; }
- .filter-bar { padding: 6px 10px; border-bottom: 1px solid var(--border); display: flex; gap: 6px; align-items: center; }
- .filter-bar .label { font-size: 11px; color: var(--accent); font-weight: 600; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
- .filter-bar select { width: auto; font-size: 11px; }
- .item-card { padding: 8px 12px; border-bottom: 1px solid var(--border); cursor: pointer; }
- .item-card:hover { background: var(--bg3); }
- .item-card.sel { background: var(--bg3); border-left: 3px solid var(--accent); }
- .item-card .num { color: var(--text2); font-size: 11px; }
- .item-card .title { font-size: 12px; font-weight: 500; }
- .item-card .meta { font-size: 10px; color: var(--text2); display: flex; gap: 8px; margin-top: 2px; }
- .label-tag { display: inline-block; font-size: 9px; font-weight: 600; padding: 0 5px; border-radius: 8px; margin-right: 3px; }
- /* ─── Detail ──────────────────────────────── */
- .detail-header h2 { font-size: 15px; margin-bottom: 2px; }
- .detail-header .meta { font-size: 11px; color: var(--text2); margin-bottom: 10px; }
- .detail-body {
- background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
- padding: 12px; margin-bottom: 12px; font-size: 12px; line-height: 1.6; word-wrap: break-word;
- }
- .detail-body pre { background: var(--bg); padding: 8px; border-radius: 3px; overflow-x: auto; font-family: var(--mono); font-size: 11px; margin: 6px 0; }
- .detail-body code { font-family: var(--mono); font-size: 11px; background: var(--bg3); padding: 1px 3px; border-radius: 2px; }
- .detail-body pre code { background: none; padding: 0; }
- .comment { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; margin-bottom: 6px; }
- .comment .author { font-weight: 600; font-size: 12px; }
- .comment .time { color: var(--text2); font-size: 10px; margin-left: 6px; }
- .comment .body { margin-top: 6px; font-size: 12px; line-height: 1.5; }
- .reply-area { margin-top: 12px; }
- .reply-actions { display: flex; gap: 6px; margin-top: 6px; align-items: center; }
- .confidence { font-size: 10px; padding: 1px 6px; border-radius: 8px; font-weight: 600; }
- .confidence.high { background: #0d3321; color: var(--green); }
- .confidence.medium { background: #3d2e00; color: var(--yellow); }
- .confidence.low { background: #3d1214; color: var(--red); }
- .empty { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text2); flex-direction: column; gap: 6px; padding: 30px; text-align: center; }
- /* ─── Notifications ───────────────────────── */
- .notif-item {
- display: flex; gap: 10px; padding: 8px 12px; background: var(--bg2);
- border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 6px; align-items: flex-start;
- }
- .notif-item .icon { font-size: 14px; margin-top: 2px; }
- .notif-item .info { flex: 1; }
- .notif-item .repo { font-size: 10px; color: var(--text2); }
- .notif-item .title { font-size: 12px; }
- .notif-item .reason { font-size: 10px; color: var(--text2); }
- /* ─── Push ────────────────────────────────── */
- .push-panel { max-width: 600px; }
- .field { margin-bottom: 10px; }
- .field label { font-size: 11px; color: var(--text2); margin-bottom: 3px; display: block; }
- .field-row { display: flex; gap: 6px; }
- .git-info { margin: 10px 0; }
- .git-info .branch { font-size: 13px; font-weight: 600; color: var(--green); }
- .git-info .counts { font-size: 11px; color: var(--text2); margin-top: 2px; }
- .git-info pre { background: var(--bg2); padding: 8px; border-radius: var(--radius); font-family: var(--mono); font-size: 11px; margin-top: 6px; max-height: 150px; overflow-y: auto; }
- .log { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px; font-family: var(--mono); font-size: 11px; min-height: 60px; white-space: pre-wrap; color: var(--text2); margin-top: 10px; }
- .log .ok { color: var(--green); }
- .log .err { color: var(--red); }
- /* ─── Publish Tab ─────────────────────────── */
- .pub-top { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-shrink: 0; }
- .pub-top h2 { font-size: 14px; flex: 1; }
- .pub-settings { display: flex; gap: 6px; align-items: center; flex-shrink: 0; margin-bottom: 8px; }
- .pub-settings select, .pub-settings input { width: auto; font-size: 11px; }
- .pub-settings input { max-width: 180px; }
- .pub-split { display: flex; flex: 1; gap: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius); }
- .pub-editor { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); }
- .pub-toolbar { display: flex; gap: 2px; padding: 4px 6px; background: var(--bg2); border-bottom: 1px solid var(--border); flex-wrap: wrap; }
- .pub-toolbar button { font-size: 11px; padding: 2px 7px; min-width: 26px; border: none; background: transparent; color: var(--text2); }
- .pub-toolbar button:hover { background: var(--bg3); color: var(--text); border-radius: 3px; }
- .pub-toolbar .sep { width: 1px; background: var(--border); margin: 2px 4px; }
- .pub-editor textarea { flex: 1; border: none; border-radius: 0; resize: none; padding: 10px; font-family: var(--mono); font-size: 12px; line-height: 1.6; background: var(--bg); }
- .pub-preview { flex: 1; overflow-y: auto; padding: 16px; background: var(--bg); font-size: 13px; line-height: 1.7; }
- .pub-preview h1 { font-size: 22px; border-bottom: 1px solid var(--border); padding-bottom: 6px; margin: 16px 0 10px; }
- .pub-preview h2 { font-size: 18px; border-bottom: 1px solid var(--border); padding-bottom: 4px; margin: 14px 0 8px; }
- .pub-preview h3 { font-size: 15px; margin: 12px 0 6px; }
- .pub-preview h4 { font-size: 13px; margin: 10px 0 4px; }
- .pub-preview p { margin: 6px 0; }
- .pub-preview img { max-width: 100%; border-radius: 4px; margin: 8px 0; }
- .pub-preview pre { background: var(--bg2); padding: 10px; border-radius: 4px; overflow-x: auto; font-family: var(--mono); font-size: 11px; margin: 8px 0; }
- .pub-preview code { font-family: var(--mono); font-size: 11px; background: var(--bg3); padding: 1px 4px; border-radius: 2px; }
- .pub-preview pre code { background: none; padding: 0; }
- .pub-preview blockquote { border-left: 3px solid var(--accent); padding: 4px 12px; margin: 8px 0; color: var(--text2); }
- .pub-preview table { border-collapse: collapse; margin: 8px 0; width: 100%; }
- .pub-preview th, .pub-preview td { border: 1px solid var(--border); padding: 4px 8px; font-size: 12px; text-align: left; }
- .pub-preview th { background: var(--bg2); }
- .pub-preview ul, .pub-preview ol { padding-left: 20px; margin: 6px 0; }
- .pub-preview li { margin: 2px 0; }
- .pub-preview hr { border: none; border-top: 1px solid var(--border); margin: 12px 0; }
- .pub-preview a { color: var(--accent); }
- .img-upload-area { padding: 6px 10px; background: var(--bg2); border-top: 1px solid var(--border); font-size: 11px; color: var(--text2); display: flex; align-items: center; gap: 6px; }
- .img-upload-area button { font-size: 11px; padding: 2px 8px; }
- /* ─── Modal ───────────────────────────────── */
- .modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 99; }
- .modal-bg.show { display: flex; }
- .modal { background: var(--bg2); border: 1px solid var(--border); border-radius: 6px; padding: 18px; width: 380px; }
- .modal h3 { margin-bottom: 12px; font-size: 14px; }
- .modal .actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 14px; }
- .spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .5s linear infinite; }
- @keyframes spin { to { transform: rotate(360deg); } }
- .loading { display: flex; align-items: center; gap: 6px; padding: 20px; color: var(--text2); }
- </style>
- </head>
- <body>
- <div class="header">
- <h1>GH-<b>Hub</b></h1>
- <span class="dot" id="hDot"></span>
- <span class="user" id="hUser">...</span>
- <span class="spacer"></span>
- <span class="badge zero" id="hBadge" onclick="switchTab('notifications')">0</span>
- </div>
- <div class="tabs">
- <div class="tab active" data-t="repos" onclick="switchTab('repos')">Repos</div>
- <div class="tab" data-t="issues" onclick="switchTab('issues')">Issues</div>
- <div class="tab" data-t="pulls" onclick="switchTab('pulls')">PRs</div>
- <div class="tab" data-t="notifications" onclick="switchTab('notifications')">Notifications</div>
- <div class="tab" data-t="push" onclick="switchTab('push')">Push</div>
- <div class="tab" data-t="publish" onclick="switchTab('publish')">Publish</div>
- </div>
- <div class="content">
- <!-- ═══ Repos ═══ -->
- <div class="panel active" id="p-repos">
- <div class="toolbar">
- <h2>Repositories</h2>
- <button onclick="loadRepos()">Refresh</button>
- <button class="primary" onclick="showModal('createRepoModal')">+ New</button>
- </div>
- <div class="repo-grid" id="repoGrid"><div class="loading"><span class="spinner"></span> Loading...</div></div>
- </div>
- <!-- ═══ Issues ═══ -->
- <div class="panel" id="p-issues" style="padding:0;">
- <div class="split">
- <div class="list-pane">
- <div class="filter-bar">
- <span class="label" id="iRepoLabel">Select a repo</span>
- <select id="iState" onchange="loadIssues()"><option value="open">Open</option><option value="closed">Closed</option><option value="all">All</option></select>
- </div>
- <div id="iList" style="flex:1;overflow-y:auto;"><div class="empty">Click a repo first</div></div>
- </div>
- <div class="detail-pane" id="iDetail"><div class="empty">Select an issue</div></div>
- </div>
- </div>
- <!-- ═══ PRs ═══ -->
- <div class="panel" id="p-pulls" style="padding:0;">
- <div class="split">
- <div class="list-pane">
- <div class="filter-bar">
- <span class="label" id="pRepoLabel">Select a repo</span>
- <select id="pState" onchange="loadPRs()"><option value="open">Open</option><option value="closed">Closed</option><option value="all">All</option></select>
- </div>
- <div id="pList" style="flex:1;overflow-y:auto;"><div class="empty">Click a repo first</div></div>
- </div>
- <div class="detail-pane" id="pDetail"><div class="empty">Select a PR</div></div>
- </div>
- </div>
- <!-- ═══ Notifications ═══ -->
- <div class="panel" id="p-notifications">
- <div class="toolbar">
- <h2>Notifications</h2>
- <button onclick="loadNotifs()">Refresh</button>
- <button onclick="markAllRead()">Mark All Read</button>
- </div>
- <div id="notifList" style="flex:1;overflow-y:auto;max-width:650px;"><div class="loading"><span class="spinner"></span> Loading...</div></div>
- </div>
- <!-- ═══ Push ═══ -->
- <div class="panel" id="p-push">
- <h2 style="font-size:14px;margin-bottom:10px;">Quick Push</h2>
- <div class="push-panel">
- <div class="field">
- <label>Local Directory</label>
- <div class="field-row">
- <input id="pushDir" placeholder="/path/to/project">
- <button onclick="pickFolder()" style="white-space:nowrap;">Browse</button>
- <button onclick="useWorkspace()" style="white-space:nowrap;">Workspace</button>
- </div>
- </div>
- <button onclick="checkGit()">Check Status</button>
- <div class="git-info" id="gitInfo"></div>
- <div style="display:flex;gap:6px;margin-top:8px;">
- <button class="primary" id="pushBtn" onclick="doPush()" disabled>Push</button>
- <button onclick="doQuickSetup()">Quick Setup + Push</button>
- </div>
- <div class="log" id="pushLog" style="display:none;"></div>
- </div>
- </div>
- <!-- ═══ Publish ═══ -->
- <div class="panel" id="p-publish">
- <div class="pub-top">
- <h2>Publish to GitHub</h2>
- <span id="pubResult" style="font-size:12px;"></span>
- </div>
- <div class="pub-settings">
- <select id="pubRepo" onchange="onRepoChange()"><option value="">Target Repo</option></select>
- <input id="pubPath" value="README.md" style="max-width:140px;" placeholder="Path (e.g. README.md)">
- <input id="pubMsg" value="Update README" style="max-width:200px;" placeholder="Commit message">
- <button onclick="fetchExistingReadme()">Load Existing</button>
- <button class="primary" onclick="doPublish()" id="pubBtn">Publish</button>
- </div>
- <div class="pub-split">
- <div class="pub-editor">
- <div class="pub-toolbar">
- <button onclick="tbWrap('**','**')" title="Bold"><b>B</b></button>
- <button onclick="tbWrap('*','*')" title="Italic"><i>I</i></button>
- <button onclick="tbWrap('~~','~~')" title="Strikethrough"><s>S</s></button>
- <div class="sep"></div>
- <button onclick="tbPrefix('# ')" title="H1">H1</button>
- <button onclick="tbPrefix('## ')" title="H2">H2</button>
- <button onclick="tbPrefix('### ')" title="H3">H3</button>
- <div class="sep"></div>
- <button onclick="tbPrefix('- ')" title="Bullet list">•</button>
- <button onclick="tbPrefix('1. ')" title="Numbered list">1.</button>
- <button onclick="tbPrefix('> ')" title="Blockquote">></button>
- <div class="sep"></div>
- <button onclick="tbLink()" title="Insert link">Link</button>
- <button onclick="tbImage()" title="Insert image from URL">Img</button>
- <button onclick="tbUploadImage()" title="Upload image to repo">Upload</button>
- <div class="sep"></div>
- <button onclick="tbWrap('`','`')" title="Inline code">code</button>
- <button onclick="tbCodeBlock()" title="Code block">```</button>
- <button onclick="tbPrefix('---')" title="Horizontal rule">HR</button>
- <div class="sep"></div>
- <button onclick="tbTable()" title="Insert table">Table</button>
- </div>
- <textarea id="pubEditor" placeholder="Write your markdown here..." oninput="updatePreview()"></textarea>
- <div class="img-upload-area">
- <span>Images:</span>
- <button onclick="tbUploadImage()">Browse & Upload</button>
- <span id="imgStatus" style="color:var(--text2);font-size:10px;"></span>
- </div>
- </div>
- <div class="pub-preview" id="pubPreview">
- <div class="empty" style="height:auto;">Live preview will appear here</div>
- </div>
- </div>
- </div>
- </div>
- <!-- ═══ Create Repo Modal ═══ -->
- <div class="modal-bg" id="createRepoModal">
- <div class="modal">
- <h3>New Repository</h3>
- <div class="field"><label>Name</label><input id="nrName" placeholder="my-project"></div>
- <div class="field"><label>Description</label><input id="nrDesc" placeholder="optional"></div>
- <div class="field"><label><input type="checkbox" id="nrPrivate" style="width:auto;margin-right:4px;">Private</label></div>
- <div class="actions">
- <button onclick="hideModal('createRepoModal')">Cancel</button>
- <button class="primary" onclick="createRepo()">Create</button>
- </div>
- </div>
- </div>
- <!-- ═══ Quick Setup Modal ═══ -->
- <div class="modal-bg" id="quickSetupModal">
- <div class="modal">
- <h3>Quick Setup + Push</h3>
- <p style="font-size:11px;color:var(--text2);margin-bottom:10px;">Create repo, init git, commit all, and push in one step.</p>
- <div class="field"><label>Repo Name (owner/name)</label><input id="qsName" placeholder="GeneralNoCodeDP/my-project"></div>
- <div class="field"><label>Description</label><input id="qsDesc"></div>
- <div class="field"><label>Commit Message</label><input id="qsMsg" value="Initial commit"></div>
- <div class="field"><label><input type="checkbox" id="qsPrivate" style="width:auto;margin-right:4px;">Private</label></div>
- <div class="actions">
- <button onclick="hideModal('quickSetupModal')">Cancel</button>
- <button class="primary" onclick="execQuickSetup()">Go</button>
- </div>
- </div>
- </div>
- <script>
- const vscode = acquireVsCodeApi();
- let S = { repo: null, repos: [], aiReady: false, pubContent: '', pendingCallbacks: {} };
- let msgId = 0;
- // ─── Bridge ──────────────────────────────────
- function send(command, args = {}) {
- return new Promise((resolve, reject) => {
- const id = ++msgId;
- S.pendingCallbacks[id] = { resolve, reject };
- vscode.postMessage({ id, command, args });
- });
- }
- window.addEventListener('message', e => {
- const msg = e.data;
- // Async response
- if (msg.id && S.pendingCallbacks[msg.id]) {
- const cb = S.pendingCallbacks[msg.id];
- delete S.pendingCallbacks[msg.id];
- if (msg.error) cb.reject(new Error(msg.error));
- else cb.resolve(msg.data);
- return;
- }
- // Push events
- if (msg.command === 'notifUpdate') {
- const n = Array.isArray(msg.data) ? msg.data.length : 0;
- setBadge(n);
- }
- });
- // ─── Tabs ────────────────────────────────────
- function switchTab(t) {
- document.querySelectorAll('.tab').forEach(el => el.classList.toggle('active', el.dataset.t === t));
- document.querySelectorAll('.panel').forEach(el => el.classList.toggle('active', el.id === `p-${t}`));
- if (t === 'notifications') loadNotifs();
- if (t === 'publish') populateRepoSelect();
- }
- // ─── Init ────────────────────────────────────
- async function init() {
- try {
- const s = await send('status');
- document.getElementById('hDot').className = s.ok ? 'dot ok' : 'dot err';
- document.getElementById('hUser').textContent = s.ok ? s.user : 'Not authenticated';
- S.aiReady = s.aiReady;
- } catch (e) {
- document.getElementById('hDot').className = 'dot err';
- document.getElementById('hUser').textContent = 'Error';
- }
- loadRepos();
- }
- function setBadge(n) {
- const b = document.getElementById('hBadge');
- b.textContent = n;
- b.className = n > 0 ? 'badge' : 'badge zero';
- }
- function showModal(id) { document.getElementById(id).classList.add('show'); }
- function hideModal(id) { document.getElementById(id).classList.remove('show'); }
- // ─── Repos ───────────────────────────────────
- const LC = { JavaScript:'#f1e05a', TypeScript:'#3178c6', Python:'#3572a5', Go:'#00add8', Rust:'#dea584', HTML:'#e34c26', CSS:'#563d7c', Shell:'#89e051', Vue:'#41b883' };
- async function loadRepos() {
- const g = document.getElementById('repoGrid');
- g.innerHTML = '<div class="loading"><span class="spinner"></span> Loading...</div>';
- try {
- const repos = await send('listRepos');
- S.repos = repos || [];
- if (!repos?.length) { g.innerHTML = '<div class="empty">No repos</div>'; return; }
- g.innerHTML = repos.map(r => {
- const nwo = r.owner.login + '/' + r.name;
- const l = r.primaryLanguage?.name || '';
- const c = LC[l] || '#858585';
- return `<div class="repo-card${S.repo===nwo?' sel':''}" onclick="selectRepo('${nwo}')" data-r="${nwo}">
- <h3>${r.name}</h3>
- <div class="desc">${esc(r.description||'No description')}</div>
- <div class="repo-meta">
- ${l?`<span><span class="lang-dot" style="background:${c}"></span>${l}</span>`:''}
- <span>★${r.stargazerCount}</span>
- <span>${timeAgo(r.updatedAt)}</span>
- ${r.isPrivate?'<span class="tag-private">Private</span>':''}
- </div></div>`;
- }).join('');
- } catch (e) { g.innerHTML = `<div class="empty">Error: ${esc(e.message)}</div>`; }
- }
- function selectRepo(nwo) {
- S.repo = nwo;
- document.querySelectorAll('.repo-card').forEach(c => c.classList.toggle('sel', c.dataset.r === nwo));
- document.getElementById('iRepoLabel').textContent = nwo;
- document.getElementById('pRepoLabel').textContent = nwo;
- loadIssues(); loadPRs();
- }
- async function createRepo() {
- const name = document.getElementById('nrName').value.trim();
- if (!name) return;
- try {
- await send('createRepo', { name, description: document.getElementById('nrDesc').value.trim(), isPrivate: document.getElementById('nrPrivate').checked });
- hideModal('createRepoModal');
- document.getElementById('nrName').value = '';
- document.getElementById('nrDesc').value = '';
- loadRepos();
- } catch (e) { alert('Error: ' + e.message); }
- }
- // ─── Issues ──────────────────────────────────
- async function loadIssues() {
- if (!S.repo) return;
- const el = document.getElementById('iList');
- el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
- try {
- const items = await send('listIssues', { repo: S.repo, state: document.getElementById('iState').value });
- if (!items?.length) { el.innerHTML = '<div class="empty">No issues</div>'; return; }
- el.innerHTML = items.map(i => `<div class="item-card" onclick="viewIssue(${i.number})">
- <div class="num">#${i.number}</div>
- <div class="title">${esc(i.title)}</div>
- <div class="meta"><span>${i.author?.login||'?'}</span><span>${timeAgo(i.createdAt)}</span><span>${i.comments||0} comments</span></div>
- ${(i.labels||[]).map(l=>`<span class="label-tag" style="background:${l.color?'#'+l.color+'33':'var(--bg3)'};color:${l.color?'#'+l.color:'var(--text2)'}">${esc(l.name)}</span>`).join('')}
- </div>`).join('');
- } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
- }
- async function viewIssue(num) {
- const el = document.getElementById('iDetail');
- el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
- document.querySelectorAll('#iList .item-card').forEach(c => c.classList.toggle('sel', c.querySelector('.num')?.textContent === '#'+num));
- try {
- const d = await send('viewIssue', { repo: S.repo, number: num });
- const cm = d.commentList || [];
- el.innerHTML = `
- <div class="detail-header"><h2>${esc(d.title)} <span style="color:var(--text2);font-weight:400">#${d.number}</span></h2>
- <div class="meta"><span style="color:${d.state==='OPEN'?'var(--green)':'var(--purple)'}">${d.state}</span> · ${d.author?.login||'?'} · ${timeAgo(d.createdAt)}</div></div>
- <div class="detail-body">${renderMd(d.body||'(empty)')}</div>
- ${cm.length?cm.map(c=>`<div class="comment"><span class="author">${esc(c.author)}</span><span class="time">${timeAgo(c.createdAt)}</span><div class="body">${renderMd(c.body)}</div></div>`).join(''):''}
- <div class="reply-area"><textarea id="iReply" placeholder="Write a reply..."></textarea>
- <div class="reply-actions">
- ${S.aiReady?`<button class="ai" onclick="aiDraft('issue',${num})">AI Draft</button>`:''}<span id="iConf"></span><span style="flex:1"></span>
- <button class="primary" onclick="postIssueComment(${num})">Reply</button>
- ${d.state==='OPEN'?`<button class="danger" onclick="doCloseIssue(${num})">Close</button>`:`<button onclick="doReopenIssue(${num})">Reopen</button>`}
- </div></div>`;
- } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
- }
- async function postIssueComment(num) {
- const body = document.getElementById('iReply').value.trim();
- if (!body) return;
- await send('commentIssue', { repo: S.repo, number: num, body });
- viewIssue(num);
- }
- async function doCloseIssue(num) { await send('closeIssue', { repo: S.repo, number: num }); viewIssue(num); loadIssues(); }
- async function doReopenIssue(num) { await send('reopenIssue', { repo: S.repo, number: num }); viewIssue(num); loadIssues(); }
- // ─── PRs ─────────────────────────────────────
- async function loadPRs() {
- if (!S.repo) return;
- const el = document.getElementById('pList');
- el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
- try {
- const items = await send('listPRs', { repo: S.repo, state: document.getElementById('pState').value });
- if (!items?.length) { el.innerHTML = '<div class="empty">No PRs</div>'; return; }
- el.innerHTML = items.map(p => `<div class="item-card" onclick="viewPR(${p.number})">
- <div class="num">#${p.number}${p.isDraft?' <span style="color:var(--text2)">(Draft)</span>':''}</div>
- <div class="title">${esc(p.title)}</div>
- <div class="meta"><span>${p.author?.login||'?'}</span><span>${p.headRefName} → ${p.baseRefName}</span><span>${timeAgo(p.createdAt)}</span></div>
- </div>`).join('');
- } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
- }
- async function viewPR(num) {
- const el = document.getElementById('pDetail');
- el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
- document.querySelectorAll('#pList .item-card').forEach(c => c.classList.toggle('sel', c.querySelector('.num')?.textContent?.startsWith('#'+num)));
- try {
- const d = await send('viewPR', { repo: S.repo, number: num });
- const cm = d.commentList || [];
- const sc = d.state==='OPEN'?'var(--green)':d.state==='MERGED'?'var(--purple)':'var(--red)';
- el.innerHTML = `
- <div class="detail-header"><h2>${esc(d.title)} <span style="color:var(--text2);font-weight:400">#${d.number}</span></h2>
- <div class="meta"><span style="color:${sc}">${d.state}</span> · ${d.author?.login||'?'} · ${d.headRefName}→${d.baseRefName} · +${d.additions||0} -${d.deletions||0} (${d.changedFiles||0} files)</div></div>
- <div class="detail-body">${renderMd(d.body||'(empty)')}</div>
- ${cm.length?cm.map(c=>`<div class="comment"><span class="author">${esc(c.author)}</span><span class="time">${timeAgo(c.createdAt)}</span><div class="body">${renderMd(c.body)}</div></div>`).join(''):''}
- <div class="reply-area"><textarea id="pReply" placeholder="Write a comment..."></textarea>
- <div class="reply-actions">
- ${S.aiReady?`<button class="ai" onclick="aiDraft('pr',${num})">AI Draft</button>`:''}<span id="pConf"></span><span style="flex:1"></span>
- <button class="primary" onclick="postPRComment(${num})">Comment</button>
- ${d.state==='OPEN'?`<button class="primary" onclick="doMergePR(${num})">Merge</button><button class="danger" onclick="doClosePR(${num})">Close</button>`:''}
- </div></div>`;
- } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
- }
- async function postPRComment(num) { const b = document.getElementById('pReply').value.trim(); if (!b) return; await send('commentPR', { repo: S.repo, number: num, body: b }); viewPR(num); }
- async function doMergePR(num) { await send('mergePR', { repo: S.repo, number: num, method: 'merge' }); viewPR(num); loadPRs(); }
- async function doClosePR(num) { await send('closePR', { repo: S.repo, number: num }); viewPR(num); loadPRs(); }
- // ─── AI Draft ────────────────────────────────
- async function aiDraft(type, num) {
- const tid = type==='issue'?'iReply':'pReply';
- const cid = type==='issue'?'iConf':'pConf';
- const ta = document.getElementById(tid);
- const conf = document.getElementById(cid);
- ta.value = 'Generating...'; ta.disabled = true;
- conf.innerHTML = '<span class="spinner"></span>';
- try {
- const detail = await send(type==='issue'?'viewIssue':'viewPR', { repo: S.repo, number: num });
- const res = await send('aiDraft', { type, title: detail.title, body: detail.body, comments: detail.commentList, repoName: S.repo });
- ta.value = res.draft;
- conf.innerHTML = `<span class="confidence ${res.confidence}">${res.confidence}</span>`;
- } catch (e) { ta.value = ''; conf.textContent = ''; alert('AI error: '+e.message); }
- ta.disabled = false; ta.focus();
- }
- // ─── Notifications ───────────────────────────
- const NI = { Issue:'<span style="color:var(--green)">●</span>', PullRequest:'<span style="color:var(--purple)">⤮</span>', Release:'<span style="color:var(--accent)">♦</span>' };
- async function loadNotifs() {
- const el = document.getElementById('notifList');
- el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
- try {
- const items = await send('listNotifications');
- setBadge(items?.length || 0);
- if (!items?.length) { el.innerHTML = '<div class="empty">No unread notifications</div>'; return; }
- el.innerHTML = items.map(n => `<div class="notif-item">
- <div class="icon">${NI[n.type]||'●'}</div>
- <div class="info"><div class="repo">${esc(n.repo)}</div><div class="title">${esc(n.title)}</div><div class="reason">${n.reason} · ${timeAgo(n.updatedAt)}</div></div>
- <button onclick="markRead('${n.id}',this.parentElement)">Read</button>
- </div>`).join('');
- } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
- }
- async function markRead(id, el) { await send('markRead', { id }); if (el) el.style.opacity = '0.3'; }
- async function markAllRead() { await send('markAllRead'); loadNotifs(); }
- // ─── Push ────────────────────────────────────
- async function pickFolder() { const p = await send('pickFolder'); if (p) document.getElementById('pushDir').value = p; }
- async function useWorkspace() { const p = await send('getWorkspaceDir'); if (p) document.getElementById('pushDir').value = p; }
- async function checkGit() {
- const dir = document.getElementById('pushDir').value.trim(); if (!dir) return;
- const el = document.getElementById('gitInfo');
- el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
- try {
- const s = await send('gitStatus', { dir });
- el.innerHTML = `<div class="branch">${esc(s.branch)}</div>
- <div class="counts">${s.ahead>0?`<span style="color:var(--green)">${s.ahead} ahead</span> `:''}${s.behind>0?`<span style="color:var(--red)">${s.behind} behind</span> `:''}${s.ahead===0&&s.behind===0?'Up to date':''}</div>
- ${s.status?`<pre>${esc(s.status)}</pre>`:'<pre style="color:var(--text2)">Clean</pre>'}`;
- document.getElementById('pushBtn').disabled = false;
- } catch (e) { el.innerHTML = `<span style="color:var(--red)">${esc(e.message)}</span>`; document.getElementById('pushBtn').disabled = true; }
- }
- async function doPush() {
- const dir = document.getElementById('pushDir').value.trim(); if (!dir) return;
- const log = document.getElementById('pushLog');
- log.style.display = 'block'; log.innerHTML = 'Pushing...\n';
- document.getElementById('pushBtn').disabled = true;
- try {
- const r = await send('gitPush', { dir });
- log.innerHTML += `<span class="ok">${esc(typeof r==='string'?r:r?.message||'Done')}</span>\n`;
- } catch (e) { log.innerHTML += `<span class="err">Error: ${esc(e.message)}</span>\n`; }
- document.getElementById('pushBtn').disabled = false;
- checkGit();
- }
- function doQuickSetup() {
- const dir = document.getElementById('pushDir').value.trim();
- if (!dir) { alert('Set directory first'); return; }
- const n = dir.split('/').pop();
- document.getElementById('qsName').value = `GeneralNoCodeDP/${n}`;
- showModal('quickSetupModal');
- }
- async function execQuickSetup() {
- const dir = document.getElementById('pushDir').value.trim();
- const log = document.getElementById('pushLog');
- log.style.display = 'block'; log.innerHTML = 'Setting up...\n';
- hideModal('quickSetupModal');
- try {
- const r = await send('quickSetup', {
- dir,
- createRepo: true,
- repoName: document.getElementById('qsName').value.trim(),
- description: document.getElementById('qsDesc').value.trim(),
- isPrivate: document.getElementById('qsPrivate').checked,
- commitMessage: document.getElementById('qsMsg').value.trim() || 'Initial commit'
- });
- log.innerHTML += `<span class="ok">${esc(r?.message||'Done!')}</span>\n`;
- } catch (e) { log.innerHTML += `<span class="err">Error: ${esc(e.message)}</span>\n`; }
- checkGit();
- }
- // ─── Publish ─────────────────────────────────
- function populateRepoSelect() {
- const sel = document.getElementById('pubRepo');
- const cur = sel.value;
- sel.innerHTML = '<option value="">Target Repo</option>' + S.repos.map(r => {
- const nwo = r.owner.login+'/'+r.name;
- return `<option value="${nwo}"${nwo===cur?' selected':''}>${nwo}</option>`;
- }).join('');
- }
- function onRepoChange() {
- // Auto-load existing README when repo changes
- }
- async function fetchExistingReadme() {
- const repo = document.getElementById('pubRepo').value;
- if (!repo) { alert('Select a repo first'); return; }
- const editor = document.getElementById('pubEditor');
- const res = document.getElementById('pubResult');
- res.innerHTML = '<span class="spinner"></span> Loading...';
- try {
- const data = await send('fetchReadme', { repo });
- if (data.content) {
- editor.value = data.content;
- updatePreview();
- res.innerHTML = '<span style="color:var(--green)">Loaded existing README</span>';
- } else {
- res.innerHTML = '<span style="color:var(--text2)">No existing README found</span>';
- }
- } catch (e) { res.innerHTML = `<span style="color:var(--red)">Error: ${esc(e.message)}</span>`; }
- }
- function updatePreview() {
- const md = document.getElementById('pubEditor').value;
- const el = document.getElementById('pubPreview');
- if (!md.trim()) { el.innerHTML = '<div class="empty" style="height:auto;">Live preview will appear here</div>'; return; }
- el.innerHTML = renderFullMd(md);
- }
- // ─── Toolbar Actions ────────────────────────
- function getEditor() { return document.getElementById('pubEditor'); }
- function tbWrap(before, after) {
- const ta = getEditor();
- const start = ta.selectionStart, end = ta.selectionEnd;
- const sel = ta.value.substring(start, end) || 'text';
- ta.value = ta.value.substring(0, start) + before + sel + after + ta.value.substring(end);
- ta.selectionStart = start + before.length;
- ta.selectionEnd = start + before.length + sel.length;
- ta.focus(); updatePreview();
- }
- function tbPrefix(prefix) {
- const ta = getEditor();
- const start = ta.selectionStart;
- // Find line start
- const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1;
- ta.value = ta.value.substring(0, lineStart) + prefix + ta.value.substring(lineStart);
- ta.selectionStart = ta.selectionEnd = start + prefix.length;
- ta.focus(); updatePreview();
- }
- function tbLink() {
- const ta = getEditor();
- const sel = ta.value.substring(ta.selectionStart, ta.selectionEnd) || 'link text';
- const start = ta.selectionStart;
- const ins = `[${sel}](url)`;
- ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
- // Select "url" for easy replacement
- ta.selectionStart = start + sel.length + 3;
- ta.selectionEnd = start + sel.length + 6;
- ta.focus(); updatePreview();
- }
- function tbImage() {
- const ta = getEditor();
- const start = ta.selectionStart;
- const ins = '';
- ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
- ta.selectionStart = start + 12;
- ta.selectionEnd = start + 21;
- ta.focus(); updatePreview();
- }
- function tbCodeBlock() {
- const ta = getEditor();
- const sel = ta.value.substring(ta.selectionStart, ta.selectionEnd) || 'code here';
- const start = ta.selectionStart;
- const ins = '```\n' + sel + '\n```';
- ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
- ta.selectionStart = start + 4;
- ta.selectionEnd = start + 4 + sel.length;
- ta.focus(); updatePreview();
- }
- function tbTable() {
- const ta = getEditor();
- const start = ta.selectionStart;
- const ins = '\n| Column 1 | Column 2 | Column 3 |\n|----------|----------|----------|\n| Cell 1 | Cell 2 | Cell 3 |\n| Cell 4 | Cell 5 | Cell 6 |\n';
- ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
- ta.selectionStart = start + 3;
- ta.selectionEnd = start + 11;
- ta.focus(); updatePreview();
- }
- // ─── Image Upload ───────────────────────────
- async function tbUploadImage() {
- const repo = document.getElementById('pubRepo').value;
- if (!repo) { alert('Select a target repo first — images will be uploaded there'); return; }
- const status = document.getElementById('imgStatus');
- status.textContent = 'Picking image...';
- try {
- const img = await send('pickImage');
- if (!img) { status.textContent = ''; return; }
- status.textContent = `Uploading ${img.name} (${(img.size/1024).toFixed(1)}KB)...`;
- const filePath = `assets/images/${img.name}`;
- const result = await send('uploadImage', { repo, filePath, base64: img.base64, message: `Upload ${img.name}` });
- // Insert markdown image at cursor
- const ta = getEditor();
- const pos = ta.selectionStart;
- const imgMd = `\n`;
- ta.value = ta.value.substring(0, pos) + imgMd + ta.value.substring(ta.selectionEnd);
- ta.selectionStart = ta.selectionEnd = pos + imgMd.length;
- updatePreview();
- status.textContent = `Uploaded ${img.name}`;
- setTimeout(() => { status.textContent = ''; }, 3000);
- } catch (e) { status.textContent = `Error: ${e.message}`; }
- }
- // ─── Publish Action ─────────────────────────
- async function doPublish() {
- const content = document.getElementById('pubEditor').value;
- if (!content.trim()) { alert('Write some content first'); return; }
- const repo = document.getElementById('pubRepo').value;
- if (!repo) { alert('Select a repo'); return; }
- const fpath = document.getElementById('pubPath').value.trim() || 'README.md';
- const msg = document.getElementById('pubMsg').value.trim() || 'Update file';
- const res = document.getElementById('pubResult');
- res.innerHTML = '<span class="spinner"></span> Publishing...';
- document.getElementById('pubBtn').disabled = true;
- try {
- await send('updateFile', { repo, content, path: fpath, message: msg });
- res.innerHTML = `<span style="color:var(--green)">Published ${esc(fpath)} to ${esc(repo)}</span>`;
- } catch (e) { res.innerHTML = `<span style="color:var(--red)">Error: ${esc(e.message)}</span>`; }
- document.getElementById('pubBtn').disabled = false;
- }
- // ─── Utils ───────────────────────────────────
- function esc(s) { return s==null?'':String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
- function timeAgo(d) {
- if (!d) return '';
- const m = Math.floor((Date.now()-new Date(d))/60000);
- if (m<1) return 'just now'; if (m<60) return m+'m'; const h=Math.floor(m/60);
- if (h<24) return h+'h'; const dy=Math.floor(h/24); if (dy<30) return dy+'d';
- return new Date(d).toLocaleDateString();
- }
- function renderMd(t) {
- if (!t) return '';
- let h = esc(t);
- h = h.replace(/```(\w*)\n([\s\S]*?)```/g,'<pre><code>$2</code></pre>');
- h = h.replace(/`([^`]+)`/g,'<code>$1</code>');
- h = h.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
- h = h.replace(/\*(.+?)\*/g,'<em>$1</em>');
- h = h.replace(/^### (.+)$/gm,'<h4 style="margin:6px 0 3px">$1</h4>');
- h = h.replace(/^## (.+)$/gm,'<h3 style="margin:8px 0 4px">$1</h3>');
- h = h.replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" target="_blank">$1</a>');
- h = h.replace(/^- (.+)$/gm,'• $1');
- h = h.replace(/\n/g,'<br>');
- return h;
- }
- function renderFullMd(t) {
- if (!t) return '';
- let h = esc(t);
- // Code blocks (fenced)
- h = h.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
- // Inline code
- h = h.replace(/`([^`\n]+)`/g, '<code>$1</code>');
- // Images (before links)
- h = h.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
- // Links
- h = h.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
- // Bold + Italic
- h = h.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
- h = h.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
- h = h.replace(/\*(.+?)\*/g, '<em>$1</em>');
- h = h.replace(/~~(.+?)~~/g, '<del>$1</del>');
- // Headings
- h = h.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
- h = h.replace(/^### (.+)$/gm, '<h3>$1</h3>');
- h = h.replace(/^## (.+)$/gm, '<h2>$1</h2>');
- h = h.replace(/^# (.+)$/gm, '<h1>$1</h1>');
- // Horizontal rule
- h = h.replace(/^---+$/gm, '<hr>');
- // Blockquote
- h = h.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');
- // Tables
- h = h.replace(/^(\|.+\|)\n(\|[-| :]+\|)\n((?:\|.+\|\n?)+)/gm, (_, header, sep, body) => {
- const ths = header.split('|').filter(c=>c.trim()).map(c=>'<th>'+c.trim()+'</th>').join('');
- const rows = body.trim().split('\n').map(row => {
- const tds = row.split('|').filter(c=>c.trim()).map(c=>'<td>'+c.trim()+'</td>').join('');
- return '<tr>'+tds+'</tr>';
- }).join('');
- return '<table><thead><tr>'+ths+'</tr></thead><tbody>'+rows+'</tbody></table>';
- });
- // Unordered list
- h = h.replace(/^- (.+)$/gm, '<li>$1</li>');
- h = h.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
- // Ordered list
- h = h.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
- // Paragraphs: double newline
- h = h.replace(/\n\n/g, '</p><p>');
- // Single newlines (not inside block elements)
- h = h.replace(/\n/g, '<br>');
- // Clean up
- h = '<p>' + h + '</p>';
- h = h.replace(/<p>\s*<(h[1-4]|hr|pre|ul|ol|table|blockquote)/g, '<$1');
- h = h.replace(/<\/(h[1-4]|pre|ul|ol|table|blockquote)>\s*<\/p>/g, '</$1>');
- h = h.replace(/<p>\s*<\/p>/g, '');
- return h;
- }
- // ─── Boot ────────────────────────────────────
- init();
- </script>
- </body>
- </html>
|