@gitpagedocs/cli 1.1.54 → 1.1.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,9 +12,6 @@ export async function ensureGitHubPagesWorkflow(getCurrentGitBranch, writeText,
12
12
  run: pnpm exec next build frontend && mv frontend/out out
13
13
  working-directory: .gitpagedocs-runtime
14
14
  env:
15
- GITHUB_ACTIONS: "true"
16
- GITHUB_REPOSITORY: \${{ github.repository }}
17
- GITPAGEDOCS_REPOSITORY_SEARCH: "true"
18
15
  GITPAGEDOCS_PATH: "${pathSegment}"
19
16
 
20
17
  - name: Relocate output to custom docs path
@@ -22,14 +19,13 @@ export async function ensureGitHubPagesWorkflow(getCurrentGitBranch, writeText,
22
19
  mkdir -p .gitpagedocs-runtime/out_new/${pathSegment}
23
20
  mv .gitpagedocs-runtime/out/* .gitpagedocs-runtime/out_new/${pathSegment}/
24
21
  rm -rf .gitpagedocs-runtime/out
25
- mv .gitpagedocs-runtime/out_new .gitpagedocs-runtime/out`
22
+ mv .gitpagedocs-runtime/out_new .gitpagedocs-runtime/out
23
+
24
+ - name: Redirect site root to custom docs path
25
+ run: printf '%s' '<!doctype html><meta http-equiv="refresh" content="0; url=./${pathSegment}/"><title>Redirecting</title><a href="./${pathSegment}/">Documentation</a>' > .gitpagedocs-runtime/out/index.html`
26
26
  : ` - name: Build static site with target repository path
27
27
  run: pnpm exec next build frontend && mv frontend/out out
28
- working-directory: .gitpagedocs-runtime
29
- env:
30
- GITHUB_ACTIONS: "true"
31
- GITHUB_REPOSITORY: \${{ github.repository }}
32
- GITPAGEDOCS_REPOSITORY_SEARCH: "true"`;
28
+ working-directory: .gitpagedocs-runtime`;
33
29
  const currentBranch = getCurrentGitBranch();
34
30
  const workflowPath = ".github/workflows/gitpagedocs-pages.yml";
35
31
  const workflowContent = `name: Deploy GitPageDocs
@@ -46,11 +42,19 @@ permissions:
46
42
 
47
43
  concurrency:
48
44
  group: "pages"
49
- cancel-in-progress: true
45
+ cancel-in-progress: false
50
46
 
51
47
  jobs:
52
48
  build:
53
49
  runs-on: ubuntu-latest
50
+ permissions:
51
+ contents: read
52
+ pages: write
53
+ id-token: write
54
+ env:
55
+ GITHUB_ACTIONS: "true"
56
+ GITHUB_REPOSITORY: \${{ github.repository }}
57
+ GITPAGEDOCS_REPOSITORY_SEARCH: "true"
54
58
  steps:
55
59
  - name: Checkout target repository
56
60
  uses: actions/checkout@v4
@@ -61,15 +65,21 @@ jobs:
61
65
  - name: Setup Node
62
66
  uses: actions/setup-node@v4
63
67
  with:
64
- node-version: "20"
68
+ node-version: "24"
65
69
 
66
70
  - name: Setup Pages
67
71
  uses: actions/configure-pages@v5
72
+ with:
73
+ enablement: true
68
74
 
69
75
  - name: Prepare runtime source
70
76
  run: |
71
77
  git clone --depth 1 https://github.com/Vidigal-code/git-page-docs.git .gitpagedocs-runtime
72
78
  cp .gitpagedocs-runtime/gitpagedocs/icon.svg /tmp/default-icon.svg 2>/dev/null || true
79
+ if [ -d gitpagelayouts ]; then
80
+ rm -rf .gitpagedocs-runtime/gitpagelayouts
81
+ cp -R gitpagelayouts .gitpagedocs-runtime/gitpagelayouts
82
+ fi
73
83
  if [ -d gitpagedocs ]; then
74
84
  rm -rf .gitpagedocs-runtime/gitpagedocs
75
85
  cp -R gitpagedocs .gitpagedocs-runtime/gitpagedocs
@@ -89,40 +99,6 @@ jobs:
89
99
 
90
100
  ${buildStepsBlock}
91
101
 
92
- - name: Force root URL to docs entrypoint
93
- env:
94
- GITHUB_REPOSITORY: \${{ github.repository }}
95
- run: |
96
- node <<'NODE'
97
- const fs = require('fs');
98
- const path = require('path');
99
- const pathSegment = ${JSON.stringify(pathSegment)};
100
- const repoName = (process.env.GITHUB_REPOSITORY || '').split('/')[1] || '';
101
- const isUserPage = repoName.toLowerCase().endsWith('.github.io');
102
- const rootRedirectBase = pathSegment;
103
- const cfgPath = path.join('.gitpagedocs-runtime', 'gitpagedocs', 'config.json');
104
- if (!fs.existsSync(cfgPath)) process.exit(0);
105
- const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
106
- const defaultVersion = cfg?.site?.docsVersion || cfg?.VersionControl?.versions?.[0]?.id || '1.1.0';
107
- const defaultLang = cfg?.site?.defaultLanguage || 'en';
108
- const redirectTargetRoot = rootRedirectBase
109
- ? './' + rootRedirectBase + '/v/' + defaultVersion + '/?lang=' + defaultLang
110
- : './v/' + defaultVersion + '/?lang=' + defaultLang;
111
- const redirectTargetProject = './v/' + defaultVersion + '/?lang=' + defaultLang;
112
- const rootHtml = '<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=' + redirectTargetRoot + '"/><script>window.location.replace("' + redirectTargetRoot + '" + (window.location.search || ""));</script></head><body>Redirecting...</body></html>';
113
- fs.writeFileSync(path.join('.gitpagedocs-runtime', 'out', 'index.html'), rootHtml);
114
- const projectPathSegments = [];
115
- if (repoName && !isUserPage) projectPathSegments.push(repoName);
116
- if (pathSegment) projectPathSegments.push(pathSegment);
117
- if (projectPathSegments.length > 0) {
118
- const projectIndexPath = path.join('.gitpagedocs-runtime', 'out', ...projectPathSegments, 'index.html');
119
- if (fs.existsSync(path.dirname(projectIndexPath))) {
120
- const projectHtml = '<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=' + redirectTargetProject + '"/><script>window.location.replace("' + redirectTargetProject + '" + (window.location.search || ""));</script></head><body>Redirecting...</body></html>';
121
- fs.writeFileSync(projectIndexPath, projectHtml);
122
- }
123
- }
124
- NODE
125
-
126
102
  - name: Add .nojekyll
127
103
  run: touch .gitpagedocs-runtime/out/.nojekyll 2>/dev/null || mkdir -p .gitpagedocs-runtime/out && touch .gitpagedocs-runtime/out/.nojekyll
128
104
 
@@ -137,10 +113,17 @@ ${buildStepsBlock}
137
113
  url: \${{ steps.deployment.outputs.page_url }}
138
114
  runs-on: ubuntu-latest
139
115
  needs: build
116
+ permissions:
117
+ pages: write
118
+ id-token: write
140
119
  steps:
141
120
  - name: Deploy to GitHub Pages
142
121
  id: deployment
143
122
  uses: actions/deploy-pages@v4
123
+ with:
124
+ timeout: 900000
125
+ error_count: 30
126
+ reporting_interval: 10000
144
127
  `;
145
128
  await writeText(workflowPath, workflowContent);
146
129
  }
package/start.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "child_process";
3
3
 
4
- const env = { ...process.env, GITPAGEDOCS_REPOSITORY_SEARCH: "false" };
4
+ const env = { ...process.env, GITPAGEDOCS_REPOSITORY_SEARCH: "true" };
5
5
  // The Next app lives in frontend/, so `next build` emits to frontend/.next —
6
6
  // point `next start` at the same directory (matches `next build frontend`).
7
7
  const child = spawn("npx", ["next", "start", "frontend"], {
package/ui/logger.mjs CHANGED
@@ -1,9 +1,9 @@
1
- /** Consistent log formatting */
2
-
3
- export function logSuccess(msg) {
4
- console.log(` ✓ ${msg}`);
5
- }
6
-
7
- export function logInfo(msg) {
8
- console.log(` ${msg}`);
9
- }
1
+ /** Consistent log formatting */
2
+
3
+ export function logSuccess(msg) {
4
+ console.log(` [ok] ${msg}`);
5
+ }
6
+
7
+ export function logInfo(msg) {
8
+ console.log(` ${msg}`);
9
+ }
@@ -1,330 +0,0 @@
1
- /** Generate GitHub-style source code viewer HTML from src/, cli/, and root config files */
2
-
3
- import path from "node:path";
4
- import { readFileSync, readdirSync, existsSync } from "node:fs";
5
-
6
- function buildTreeFromKeys(fileKeys) {
7
- const root = {};
8
- for (const key of fileKeys) {
9
- const parts = key.split("/");
10
- let curr = root;
11
- for (let i = 0; i < parts.length; i++) {
12
- const part = parts[i];
13
- const isLast = i === parts.length - 1;
14
- if (!curr[part]) curr[part] = isLast ? { path: key } : {};
15
- if (isLast) curr[part].path = key;
16
- else curr = curr[part];
17
- }
18
- }
19
- return root;
20
- }
21
-
22
- const ROOT_FILES = ["README.md", "package.json", "package-lock.json", "next.config.ts", "tsconfig.json", ".eslintrc.json"];
23
-
24
- function collectSourceFiles(projectRoot) {
25
- const files = {};
26
- function scan(dir, prefix = "") {
27
- if (!existsSync(dir)) return;
28
- const entries = readdirSync(dir, { withFileTypes: true })
29
- .sort((a, b) => a.name.localeCompare(b.name));
30
- for (const e of entries) {
31
- const name = e.name;
32
- if (name === "node_modules" || name.startsWith(".")) continue;
33
- const rel = prefix ? `${prefix}/${name}` : name;
34
- const fullPath = path.join(dir, name);
35
- if (e.isDirectory()) {
36
- scan(fullPath, rel);
37
- } else {
38
- try {
39
- const content = readFileSync(fullPath, "utf-8");
40
- files[rel] = content;
41
- } catch {
42
- /* skip binary or unreadable */
43
- }
44
- }
45
- }
46
- }
47
- for (const f of ROOT_FILES) {
48
- const p = path.join(projectRoot, f);
49
- if (existsSync(p)) {
50
- try {
51
- files[f] = readFileSync(p, "utf-8");
52
- } catch {}
53
- }
54
- }
55
- const srcDir = path.join(projectRoot, "src");
56
- const cliDir = path.join(projectRoot, "cli");
57
- if (existsSync(srcDir)) scan(srcDir, "src");
58
- if (existsSync(cliDir)) scan(cliDir, "cli");
59
- return files;
60
- }
61
-
62
- /** GitHub-like file viewer: breadcrumb bar, file header, table layout, dark theme */
63
- export function generateSourceViewerHtml(root) {
64
- const files = collectSourceFiles(root);
65
- const fileKeys = Object.keys(files).sort();
66
- const tree = buildTreeFromKeys(fileKeys);
67
- const noFilesMsg = "No files found.";
68
- const dataJson = JSON.stringify({ files, fileKeys, tree });
69
- return `<!DOCTYPE html>
70
- <html lang="en">
71
- <head>
72
- <meta charset="UTF-8"/>
73
- <meta name="viewport" content="width=device-width,initial-scale=1"/>
74
- <title>Source code - GitHub</title>
75
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"/>
76
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
77
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/typescript.min.js"></script>
78
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/tsx.min.js"></script>
79
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js"></script>
80
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js"></script>
81
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/css.min.js"></script>
82
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/markdown.min.js"></script>
83
- <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/12.0.1/marked.min.js"></script>
84
- <style>
85
- /* GitHub dark theme - variables */
86
- :root{
87
- --github-bg: #0d1117;
88
- --github-bg-secondary: #161b22;
89
- --github-bg-tertiary: #21262d;
90
- --github-border: #30363d;
91
- --github-border-muted: #21262d;
92
- --github-fg: #e6edf3;
93
- --github-fg-muted: #8b949e;
94
- --github-accent: #58a6ff;
95
- --github-accent-soft: #79c0ff;
96
- }
97
- *{box-sizing:border-box;margin:0;padding:0}
98
- body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;background:var(--github-bg);color:var(--github-fg);min-height:100vh;font-size:14px;line-height:1.5}
99
-
100
- /* Top bar - GitHub style */
101
- .top-bar{background:var(--github-bg-secondary);border-bottom:1px solid var(--github-border-muted);padding:8px 16px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
102
- .top-bar .crumb{color:var(--github-fg-muted);text-decoration:none}
103
- .top-bar .crumb:hover{color:var(--github-accent)}
104
- .top-bar .crumb-sep{color:var(--github-fg-muted);user-select:none}
105
- .top-bar .file-name{color:var(--github-fg);font-weight:600}
106
-
107
- /* Layout */
108
- .layout{display:flex;height:calc(100vh - 45px)}
109
-
110
- /* Sidebar - GitHub file tree */
111
- .sidebar{width:260px;min-width:200px;background:var(--github-bg-secondary);border-right:1px solid var(--github-border-muted);overflow-y:auto;padding:8px 0;flex-shrink:0}
112
- .sidebar .folder,.sidebar .file{padding:4px 12px 4px 8px;font-size:13px;cursor:pointer;display:flex;align-items:center;gap:6px;border-radius:6px;margin:0 8px}
113
- .sidebar .folder:hover,.sidebar .file:hover{background:var(--github-bg-tertiary)}
114
- .sidebar .file{color:var(--github-fg-muted)}
115
- .sidebar .file.active{color:var(--github-accent);background:var(--github-bg-tertiary)}
116
- .sidebar .icon{width:16px;text-align:center;flex-shrink:0}
117
- .sidebar .icon svg{vertical-align:middle}
118
- .sidebar .folder .icon{color:var(--github-accent-soft)}
119
- .sidebar .chevron{width:16px;transition:transform .15s;flex-shrink:0}
120
- .sidebar .folder.collapsed .chevron{transform:rotate(-90deg)}
121
- .sidebar .children{margin-left:4px}
122
- .sidebar .children.hidden{display:none}
123
- .sidebar .search-wrap{padding:8px;border-bottom:1px solid var(--github-border-muted)}
124
- .sidebar .search-input{width:100%;padding:6px 10px;font-size:12px;border:1px solid var(--github-border);border-radius:6px;background:var(--github-bg);color:var(--github-fg);outline:none}
125
- .sidebar .search-input:focus{border-color:var(--github-accent)}
126
- .sidebar .search-input::placeholder{color:var(--github-fg-muted)}
127
- .sidebar .tree-actions{display:flex;gap:4px;padding:4px 8px;border-bottom:1px solid var(--github-border-muted)}
128
- .sidebar .tree-btn{padding:4px 8px;font-size:11px;border:1px solid var(--github-border);border-radius:4px;background:var(--github-bg-tertiary);color:var(--github-fg-muted);cursor:pointer}
129
- .sidebar .tree-btn:hover{color:var(--github-fg);background:var(--github-border)}
130
-
131
- /* Content - GitHub file view */
132
- .content{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--github-bg)}
133
- .content .file-header{background:var(--github-bg-secondary);border-bottom:1px solid var(--github-border-muted);padding:8px 16px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
134
- .content .file-header .file-path{font-weight:600;color:var(--github-fg)}
135
- .content .file-header .copy-btn{padding:4px 12px;font-size:12px;border:1px solid var(--github-border);border-radius:6px;background:var(--github-bg-tertiary);color:var(--github-fg);cursor:pointer}
136
- .content .file-header .copy-btn:hover{background:var(--github-border);border-color:var(--github-fg-muted)}
137
- .content .code-wrap{flex:1;overflow:auto}
138
- .content .code-table{width:100%;border-collapse:collapse;font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;font-size:12px;line-height:1.45}
139
- .content .code-table td{vertical-align:top;padding:0}
140
- .content .line-nums{padding:16px 0;text-align:right;user-select:none;color:var(--github-fg-muted);background:var(--github-bg-secondary);border-right:1px solid var(--github-border-muted);width:1%;min-width:50px}
141
- .content .line-nums span{display:block;padding:0 16px}
142
- .content .line-code{padding:16px 0;padding-left:16px}
143
- .content .line-code .line{display:block;min-height:1.45em;white-space:pre}
144
- .content .empty{color:var(--github-fg-muted);padding:24px;font-style:italic}
145
- .content .md-preview{padding:24px;line-height:1.6}
146
- .content .md-preview h1,.content .md-preview h2,.content .md-preview h3{margin-top:1em;margin-bottom:0.5em;font-weight:600}
147
- .content .md-preview code{background:var(--github-bg-tertiary);padding:2px 6px;border-radius:4px;font-size:0.9em}
148
- .content .md-preview pre{background:var(--github-bg-tertiary);padding:16px;overflow-x:auto;border-radius:6px;font-size:13px}
149
- .content .btn-group{display:flex;gap:8px;margin-bottom:16px}
150
- .content .btn{padding:6px 12px;border:1px solid var(--github-border);border-radius:6px;background:var(--github-bg-tertiary);color:var(--github-fg);cursor:pointer;font-size:12px}
151
- .content .btn.active{background:var(--github-accent);border-color:var(--github-accent);color:#fff}
152
- .content .btn:hover{background:var(--github-border)}
153
-
154
- @media(max-width:767px){
155
- .layout{flex-direction:column;height:auto;min-height:100vh}
156
- .sidebar{width:100%;max-height:200px;border-right:none;border-bottom:1px solid var(--github-border-muted)}
157
- .content .code-wrap{min-height:400px}
158
- }
159
- </style>
160
- </head>
161
- <body>
162
- <div class="top-bar">
163
- <span class="crumb">source</span>
164
- <span class="crumb-sep">/</span>
165
- <span class="file-name" id="breadcrumbFile">-</span>
166
- </div>
167
- <div class="layout">
168
- <aside class="sidebar">
169
- <div class="search-wrap"><input type="text" class="search-input" id="searchInput" placeholder="Filter files..." autocomplete="off"/></div>
170
- <div class="tree-actions"><button class="tree-btn" id="expandAll">Expand all</button><button class="tree-btn" id="collapseAll">Collapse all</button></div>
171
- <div id="tree"></div>
172
- </aside>
173
- <main class="content">
174
- <div class="file-header" id="fileHeader" style="display:none">
175
- <span class="file-path" id="headerPath"></span>
176
- <button class="copy-btn" id="copyBtn">Copy</button>
177
- </div>
178
- <div class="code-wrap">
179
- <pre class="content-inner" id="viewer"><code class="empty">${fileKeys.length ? "Select a file from the sidebar" : noFilesMsg}</code></pre>
180
- </div>
181
- </main>
182
- </div>
183
- <script type="application/json" id="filesData">${dataJson.replace(/<\/script>/gi, "<\\/script>")}</script>
184
- <script>
185
- (function(){
186
- var el=document.getElementById("filesData");
187
- var raw=el?el.textContent:"{}";
188
- var data=JSON.parse(raw);
189
- var files=data.files||{}, keys=data.fileKeys||[], treeData=data.tree||{};
190
- function esc(s){var d=document.createElement("div");d.textContent=s;return d.innerHTML}
191
- function getLang(p){
192
- if(p.endsWith(".ts")||p.endsWith(".tsx"))return"typescript";
193
- if(p.endsWith(".js")||p.endsWith(".mjs")||p.endsWith(".cjs"))return"javascript";
194
- if(p.endsWith(".json"))return"json";
195
- if(p.endsWith(".css"))return"css";
196
- if(p.endsWith(".md"))return"markdown";
197
- return"";
198
- }
199
- var viewMode="code";
200
- function setViewMode(mode){viewMode=mode}
201
- function selectFile(path){
202
- document.querySelectorAll(".sidebar .file.active").forEach(function(n){n.classList.remove("active")});
203
- document.querySelectorAll(".sidebar .folder.active").forEach(function(n){n.classList.remove("active")});
204
- var node=document.querySelector('.sidebar .file[data-path="'+path.replace(/"/g,'\\"')+'"]');
205
- if(node){node.classList.add("active");node.scrollIntoView({block:"nearest"})}
206
- document.getElementById("breadcrumbFile").textContent=path;
207
- document.getElementById("fileHeader").style.display="block";
208
- document.getElementById("headerPath").textContent=path;
209
- var content=files[path]||"";
210
- var isReadme=path.toLowerCase()==="readme.md";
211
- var viewer=document.getElementById("viewer");
212
- if(isReadme){
213
- var btnHtml='<div class="btn-group"><button class="btn'+(viewMode==='preview'?' active':'')+'" data-mode="preview">Preview</button><button class="btn'+(viewMode==='code'?' active':'')+'" data-mode="code">Code</button></div>';
214
- if(viewMode==="preview"&&typeof marked!=="undefined"){
215
- viewer.innerHTML=btnHtml+'<div class="md-preview">'+marked.parse(content||"")+'</div>';
216
- }else{
217
- var lines=(content||"").split("\\n");
218
- var lineHtml="";
219
- lines.forEach(function(line,i){lineHtml+="<span class=\\"line-num\\">"+(i+1)+"</span><span class=\\"line\\">"+esc(line)+"\\n</span>"});
220
- viewer.innerHTML=btnHtml+"<code>"+lineHtml+"</code>";
221
- if(typeof hljs!=="undefined"){var c=viewer.querySelector("code");if(c){c.className="language-markdown";hljs.highlightElement(c)}}
222
- }
223
- viewer.classList.remove("empty");
224
- viewer.querySelectorAll(".btn").forEach(function(btn){
225
- btn.onclick=function(){setViewMode(btn.dataset.mode);selectFile(path)};
226
- });
227
- }else{
228
- var lines=content.split("\\n");
229
- var nums="",code="";
230
- lines.forEach(function(line,i){nums+="<span>"+(i+1)+"</span>";code+="<span class=\\"line\\">"+esc(line)+"\\n</span>"});
231
- viewer.innerHTML='<table class="code-table"><tr><td class="line-nums">'+nums+'</td><td class="line-code"><code>'+code+'</code></td></tr></table>';
232
- viewer.classList.remove("empty");
233
- var c=viewer.querySelector("code");if(c&&typeof hljs!=="undefined"){c.className="language-"+getLang(path);hljs.highlightElement(c)}
234
- }
235
- window._currentPath=path;window._currentContent=content;
236
- }
237
- document.getElementById("copyBtn").onclick=function(){
238
- var p=window._currentPath,c=window._currentContent;
239
- if(!c)return;
240
- navigator.clipboard&&navigator.clipboard.writeText(c).then(function(){var b=document.getElementById("copyBtn");var t=b.textContent;b.textContent="Copied!";setTimeout(function(){b.textContent=t},1500)});
241
- };
242
- function folderIcon(){return'<svg width="16" height="16" viewBox="0 0 16 16"><path fill="currentColor" d="M.513 1.513A1.75 1.75 0 0 1 1.75 1h3.5c.58 0 1.165.224 1.587.624l.357.364A.75.75 0 0 0 8 2h5.25A1.75 1.75 0 0 1 15 3.75v8.5A1.75 1.75 0 0 1 13.25 14H1.75A1.75 1.75 0 0 1 0 12.25v-9.5C0 2.178.78 1.408 1.513 1.513Z"/></svg>'}
243
- function fileIcon(){return'<svg width="16" height="16" viewBox="0 0 16 16"><path fill="currentColor" d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Z"/></svg>'}
244
- function chevronDown(){return'<svg width="16" height="16" viewBox="0 0 16 16"><path fill="currentColor" d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z"/></svg>'}
245
- function renderTree(node, parentEl, indent){
246
- indent=indent||0;
247
- var keys=Object.keys(node).sort();
248
- for(var i=0;i<keys.length;i++){
249
- var k=keys[i];
250
- var v=node[k];
251
- if(k==="path")continue;
252
- if(v&&typeof v==="object"&&v.path){
253
- var div=document.createElement("div");
254
- div.className="file";
255
- div.dataset.path=v.path;
256
- div.style.marginLeft=(indent*12)+"px";
257
- div.innerHTML='<span class="icon">'+fileIcon()+'</span>'+esc(k);
258
- div.onclick=function(p){return function(){selectFile(p)}}(v.path);
259
- parentEl.appendChild(div);
260
- }else if(v&&typeof v==="object"){
261
- var folderDiv=document.createElement("div");
262
- folderDiv.className="folder";
263
- folderDiv.style.marginLeft=(indent*12)+"px";
264
- folderDiv.innerHTML='<span class="chevron">'+chevronDown()+'</span><span class="icon">'+folderIcon()+'</span>'+esc(k);
265
- var children=document.createElement("div");
266
- children.className="children";
267
- folderDiv.onclick=function(e){
268
- if(e.target.closest(".file"))return;
269
- var self=e.currentTarget;
270
- self.classList.toggle("collapsed");
271
- children.classList.toggle("hidden",self.classList.contains("collapsed"));
272
- };
273
- parentEl.appendChild(folderDiv);
274
- parentEl.appendChild(children);
275
- renderTree(v,children,indent+1);
276
- }
277
- }
278
- }
279
- function buildTreeFromKeys(keys){
280
- var root={};
281
- keys.forEach(function(key){
282
- var parts=key.split("/");
283
- var curr=root;
284
- for(var i=0;i<parts.length;i++){
285
- var part=parts[i];
286
- var isLast=i===parts.length-1;
287
- if(!curr[part])curr[part]=isLast?{path:key}:{};
288
- if(isLast)curr[part].path=key;
289
- else curr=curr[part];
290
- }
291
- });
292
- return root;
293
- }
294
- function buildTree(filterQuery){
295
- var treeEl=document.getElementById("tree");
296
- treeEl.innerHTML="";
297
- var filteredKeys=filterQuery?keys.filter(function(k){return k.toLowerCase().indexOf(filterQuery.toLowerCase())>=0;}):keys;
298
- var data=filteredKeys.length?buildTreeFromKeys(filteredKeys):{};
299
- if(Object.keys(data).length>0){
300
- renderTree(data,treeEl,0);
301
- }else{
302
- filteredKeys.sort();
303
- filteredKeys.forEach(function(k){
304
- var div=document.createElement("div");
305
- div.className="file";
306
- div.dataset.path=k;
307
- div.innerHTML='<span class="icon">'+fileIcon()+'</span>'+esc(k.split("/").pop());
308
- div.onclick=function(){selectFile(k)};
309
- treeEl.appendChild(div);
310
- });
311
- }
312
- }
313
- document.getElementById("searchInput").oninput=function(){
314
- var q=this.value.trim();
315
- buildTree(q);
316
- };
317
- document.getElementById("expandAll").onclick=function(){
318
- document.querySelectorAll(".sidebar .folder.collapsed").forEach(function(f){f.classList.remove("collapsed")});
319
- document.querySelectorAll(".sidebar .children.hidden").forEach(function(c){c.classList.remove("hidden")});
320
- };
321
- document.getElementById("collapseAll").onclick=function(){
322
- document.querySelectorAll(".sidebar .folder").forEach(function(f){f.classList.add("collapsed")});
323
- document.querySelectorAll(".sidebar .children").forEach(function(c){c.classList.add("hidden")});
324
- };
325
- buildTree();
326
- })();
327
- </script>
328
- </body>
329
- </html>`;
330
- }