lib/render.js - git - Webernet Explorer _X
git 🏠 Home 👤 Sign up Self hosted git, retro shell
★ otgit, code hosting from the year 2000 ★
📄 octuna / lib / render.js
173 lines · 6.9 KB · Raw
1const fs = require('fs');
2const path = require('path');
3const config = require('./config');
4 
5const VIEWS = path.join(__dirname, '..', 'views');
6const viewCache = {};
7 
8function loadView(name) {
9 if (!viewCache[name]) {
10 viewCache[name] = fs.readFileSync(path.join(VIEWS, name), 'utf8');
11 }
12 return viewCache[name];
13}
14 
15function escape(s) {
16 return String(s).replace(/[&<>"']/g, c =>
17 ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
18}
19 
20function fill(template, vars) {
21 return template.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
22 const v = vars[key];
23 return (v === undefined || v === null) ? '' : String(v);
24 });
25}
26 
27function sidebar(session) {
28 if (session) {
29 const isAdmin = session.username === config.get('admin.user');
30 const adminLink = isAdmin
31 ? '<div><a href="/admin/settings">&#9881; Admin settings</a></div>' : '';
32 return `<div class="logged-in">
33 <div>Signed in as <a href="/${escape(session.username)}"><code>${escape(session.username)}</code></a></div>
34 ${adminLink}
35 <button id="logoutBtn" type="button">Sign out</button>
36 </div>`;
37 }
38 return `<form class="login-form" id="loginForm" autocomplete="off">
39 <label for="lUser">Username:</label>
40 <input type="text" id="lUser" name="username" required autocomplete="username">
41 <label for="lPass">Password:</label>
42 <input type="password" id="lPass" name="password" required autocomplete="current-password">
43 <button type="submit">Sign in</button>
44 <div class="login-msg" id="loginMsg"></div>
45 <div style="margin-top:6px"><a href="/signup">Create account</a></div>
46 </form>`;
47}
48 
49function page(opts) {
50 const layout = loadView('layout.html');
51 const { title, content, session, addressUrl, statusText, extraSidebar } = opts;
52 return fill(layout, {
53 TITLE: escape((title ? title + ' - ' : '') + config.get('site.name')),
54 SITE_NAME: escape(config.get('site.name')),
55 TAGLINE: escape(config.get('site.tagline')),
56 MARQUEE_TEXT: config.get('ui.marqueeText'),
57 FOOTER_TEXT: escape(config.get('ui.footerText')),
58 ADDRESS_URL: escape(addressUrl || '/'),
59 CONTENT: content || '',
60 USER_PANEL: sidebar(session),
61 EXTRA_SIDEBAR: extraSidebar || '',
62 STATUS_TEXT: escape(statusText || 'Done')
63 });
64}
65 
66function breadcrumb(owner, repo, ref, treePath) {
67 const parts = treePath ? treePath.split('/').filter(Boolean) : [];
68 let html = `<a href="/${escape(owner)}/${escape(repo)}/tree/${escape(ref)}">${escape(repo)}</a>`;
69 let accumulated = '';
70 for (let i = 0; i < parts.length; i++) {
71 accumulated += (accumulated ? '/' : '') + parts[i];
72 if (i < parts.length - 1) {
73 html += ` / <a href="/${escape(owner)}/${escape(repo)}/tree/${escape(ref)}/${escape(accumulated)}">${escape(parts[i])}</a>`;
74 } else {
75 html += ` / <strong>${escape(parts[i])}</strong>`;
76 }
77 }
78 return html;
79}
80 
81function treeRows(entries, owner, repo, ref, treePath) {
82 const base = `/${escape(owner)}/${escape(repo)}`;
83 const pathPrefix = treePath ? treePath + '/' : '';
84 const rows = entries.map(e => {
85 const fullPath = pathPrefix + e.name;
86 const isDir = e.type === 'tree';
87 const href = isDir
88 ? `${base}/tree/${escape(ref)}/${escape(fullPath)}`
89 : `${base}/blob/${escape(ref)}/${escape(fullPath)}`;
90 const icon = isDir ? '&#128193;' : '&#128196;';
91 return `<tr>
92 <td class="tree-icon">${icon}</td>
93 <td class="tree-name"><a href="${href}">${escape(e.name)}${isDir ? '/' : ''}</a></td>
94 </tr>`;
95 });
96 return rows.join('\n');
97}
98 
99function commitRows(commits, owner, repo) {
100 return commits.map(c => {
101 const sha7 = c.hash.slice(0, 7);
102 const href = `/${escape(owner)}/${escape(repo)}/commit/${escape(c.hash)}`;
103 const date = c.date ? c.date.slice(0, 10) : '';
104 return `<tr>
105 <td class="commit-sha"><a href="${href}" title="${escape(c.hash)}">${escape(sha7)}</a></td>
106 <td class="commit-msg">${escape(c.message)}</td>
107 <td class="commit-author">${escape(c.author)}</td>
108 <td class="commit-date">${escape(date)}</td>
109 </tr>`;
110 }).join('\n');
111}
112 
113function blobLines(content) {
114 const lines = content.split('\n');
115 const rows = lines.map((line, i) => {
116 const n = i + 1;
117 const safe = escape(line);
118 return `<tr><td class="blob-num" id="L${n}"><a href="#L${n}">${n}</a></td><td class="blob-line">${safe || '&nbsp;'}</td></tr>`;
119 }).join('');
120 return `<table class="blob-table">${rows}</table>`;
121}
122 
123function fileTreeSidebar(entries, owner, repo, ref, currentFile) {
124 const items = entries.map(e => {
125 const isDir = e.type === 'tree';
126 const href = isDir
127 ? `/${escape(owner)}/${escape(repo)}/tree/${escape(ref)}/${escape(e.name)}`
128 : `/${escape(owner)}/${escape(repo)}/blob/${escape(ref)}/${escape(e.name)}`;
129 const active = (currentFile === e.name) ? ' class="active"' : '';
130 const icon = isDir ? '&#128193;' : '&#128196;';
131 return `<li${active}>${icon} <a href="${href}">${escape(e.name)}${isDir ? '/' : ''}</a></li>`;
132 }).join('');
133 const rootLink = `<div class="tree-sidebar-root"><a href="/${escape(owner)}/${escape(repo)}">&#8617; ${escape(repo)}</a></div>`;
134 return `<div class="panel"><div class="head">&#128193; Files</div><div class="body tree-sidebar-body">${rootLink}<ul class="tree-sidebar">${items}</ul></div></div>`;
135}
136 
137const LANG_COLOR = {
138 JavaScript: '#f1e05a', TypeScript: '#3178c6', Python: '#3572A5',
139 Ruby: '#701516', Go: '#00ADD8', Rust: '#dea584', Java: '#b07219',
140 C: '#555555', 'C++': '#f34b7d', 'C#': '#178600', PHP: '#4F5D95',
141 Shell: '#89e051', PowerShell: '#012456', Batch: '#C1F12E',
142 HTML: '#e34c26', CSS: '#563d7c', SCSS: '#c6538c', Sass: '#a53b70', Less: '#1d365d',
143 Markdown: '#083fa1', JSON: '#292929', YAML: '#cb171e',
144 XML: '#0060ac', TOML: '#9c4221', INI: '#d1dbe0', Config: '#6d8086',
145 SQL: '#e38c00',
146 Vue: '#41b883', Svelte: '#ff3e00',
147 Swift: '#F05138', Kotlin: '#A97BFF', Dart: '#00B4AB',
148 Elixir: '#6e4a7e', Erlang: '#B83998',
149 Scala: '#c22d40', Clojure: '#db5855', Elm: '#60B5CC',
150 Lua: '#000080', Perl: '#0298c3', R: '#198CE7',
151 Text: '#cccccc', TeX: '#3D6117', Other: '#888888'
152};
153 
154function langColor(name) { return LANG_COLOR[name] || LANG_COLOR.Other; }
155 
156function langBar(stats) {
157 if (!stats || stats.length === 0) return '';
158 const top = stats.slice(0, 6);
159 const segs = top.map(s =>
160 `<span class="lang-segment" style="background:${langColor(s.lang)};width:${s.pct.toFixed(2)}%" title="${escape(s.lang)} ${s.pct.toFixed(1)}%"></span>`
161 ).join('');
162 const labels = top.map(s =>
163 `<span class="lang-label"><span class="lang-dot" style="background:${langColor(s.lang)}"></span> ${escape(s.lang)} <span class="lang-pct">${s.pct.toFixed(1)}%</span></span>`
164 ).join('');
165 return `<div class="lang-section"><div class="lang-bar">${segs}</div><div class="lang-labels">${labels}</div></div>`;
166}
167 
168module.exports = {
169 escape, fill, page, loadView,
170 sidebar, breadcrumb, treeRows, commitRows, blobLines, fileTreeSidebar,
171 langBar, langColor
172};
173 
Done 🔒 Internet