pboProperty Ontology
javascript158 lines7.9 KB
RawDownload
1'use strict';
2
3// HTML page templates. Plain template strings; every dynamic value is
4// escaped by the caller or here.
5
6const { escapeHtml, encodePath, formatBytes } = require('./render');
7
8const SITE = {
9 title: 'Property Ontology',
10 subtitle: 'Australian Property Investment, Accommodation and Operations Ontology (pbo)',
11};
12
13const FILE_ICONS = {
14 md: 'M', markdown: 'M', py: 'Py', js: 'JS', mjs: 'JS', json: '{}', jsonl: '{}', yaml: 'Y', yml: 'Y',
15 ttl: 'RDF', rq: 'SPARQL', sh: '$', html: '<>', svg: 'SVG', txt: 'T', docx: 'DOC', zip: 'ZIP',
16};
17
18function extOf(name) {
19 const i = name.lastIndexOf('.');
20 return i === -1 ? '' : name.slice(i + 1).toLowerCase();
21}
22
23function iconFor(name) {
24 return FILE_ICONS[extOf(name)] || '·';
25}
26
27function layout({ title, body, activeNav, sidebar = '', aside = '', bodyClass = '', head = '' }) {
28 const nav = [
29 ['/', 'Home', 'home'],
30 ['/report', 'Board report', 'report'],
31 ['/atlas', 'Semantic atlas', 'atlas'],
32 ['/files', 'Files', 'files'],
33 ]
34 .map(([href, label, key]) => `<a href="${href}" class="nav-link${activeNav === key ? ' active' : ''}">${label}</a>`)
35 .join('');
36 return `<!doctype html>
37<html lang="en">
38<head>
39<meta charset="utf-8">
40<meta name="viewport" content="width=device-width,initial-scale=1">
41<title>${escapeHtml(title ? `${title} · ${SITE.title}` : SITE.title)}</title>
42<meta name="description" content="${escapeHtml(SITE.subtitle)}">
43<link rel="icon" href="/favicon.svg" type="image/svg+xml">
44<link rel="stylesheet" href="/vendor/hljs/github.css" media="(prefers-color-scheme: light)" data-theme-css="light">
45<link rel="stylesheet" href="/vendor/hljs/github-dark.css" media="(prefers-color-scheme: dark)" data-theme-css="dark">
46<link rel="stylesheet" href="/static/style.css">
47<script>(function(){try{var t=localStorage.getItem('theme');if(t){document.documentElement.dataset.theme=t;}}catch(e){}})();</script>
48${head}
49</head>
50<body class="${escapeHtml(bodyClass)}">
51<header class="topbar">
52 <a class="brand" href="/"><span class="brand-mark">pbo</span><span class="brand-text">${SITE.title}</span></a>
53 <nav class="topnav">${nav}</nav>
54 <div class="topbar-actions">
55 <button type="button" class="icon-btn" id="search-open" title="Find file (press /)"><span class="kbd">/</span> Find file</button>
56 <button type="button" class="icon-btn" id="theme-toggle" title="Toggle theme" aria-label="Toggle theme">◐</button>
57 </div>
58</header>
59<div class="shell${sidebar ? ' has-sidebar' : ''}${aside ? ' has-aside' : ''}">
60 ${sidebar ? `<aside class="sidebar" id="sidebar">${sidebar}</aside>` : ''}
61 <main class="main" id="main">${body}</main>
62 ${aside ? `<aside class="aside" id="aside">${aside}</aside>` : ''}
63</div>
64<dialog id="search-dialog" class="search-dialog">
65 <form method="dialog" class="search-form">
66 <input type="search" id="search-input" placeholder="Type to find a file…" autocomplete="off" spellcheck="false">
67 <button type="submit" class="icon-btn" aria-label="Close">Esc</button>
68 </form>
69 <ul id="search-results" class="search-results"></ul>
70</dialog>
71<footer class="footer">
72 <span>${escapeHtml(SITE.subtitle)}</span>
73 <span><a href="https://github.com/rdb420/property_ontology" target="_blank" rel="noopener">GitHub</a></span>
74</footer>
75<script src="/static/app.js" defer></script>
76</body>
77</html>`;
78}
79
80/* ------------------------------------------------------------------ */
81
82function breadcrumbs(relPath, isDir) {
83 const parts = relPath ? relPath.split('/') : [];
84 const items = [`<a href="/files">repo</a>`];
85 let acc = '';
86 parts.forEach((p, i) => {
87 acc = acc ? `${acc}/${p}` : p;
88 const last = i === parts.length - 1;
89 if (last && !isDir) items.push(`<span class="crumb-current">${escapeHtml(p)}</span>`);
90 else if (last) items.push(`<a class="crumb-current" href="/files/${encodePath(acc)}">${escapeHtml(p)}</a>`);
91 else items.push(`<a href="/files/${encodePath(acc)}">${escapeHtml(p)}</a>`);
92 });
93 return `<nav class="crumbs" aria-label="Breadcrumb">${items.join('<span class="crumb-sep">/</span>')}</nav>`;
94}
95
96/** Nested sidebar tree. Directories containing the current path are open. */
97function sidebarTree(root, currentPath) {
98 const openDirs = new Set();
99 if (currentPath) {
100 const parts = currentPath.split('/');
101 let acc = '';
102 for (const p of parts) {
103 acc = acc ? `${acc}/${p}` : p;
104 openDirs.add(acc);
105 }
106 }
107 const renderNode = (node, depth) => {
108 if (node.type === 'dir') {
109 const open = depth === 0 || openDirs.has(node.path);
110 const kids = node.children.map((c) => renderNode(c, depth + 1)).join('');
111 const label = depth === 0 ? 'repo' : escapeHtml(node.name);
112 return `<li class="tree-dir" data-path="${escapeHtml(node.path)}"><details${open ? ' open' : ''}><summary><span class="tree-caret"></span><a href="/files/${encodePath(node.path)}" class="tree-link dir${node.path === currentPath ? ' current' : ''}">${label}</a><span class="tree-count">${node.fileCount}</span></summary><ul>${kids}</ul></details></li>`;
113 }
114 return `<li class="tree-file" data-path="${escapeHtml(node.path)}" data-name="${escapeHtml(node.name.toLowerCase())}"><a href="/files/${encodePath(node.path)}" class="tree-link file${node.path === currentPath ? ' current' : ''}"><span class="fi fi-${escapeHtml(extOf(node.name) || 'none')}">${iconFor(node.name)}</span><span class="tree-name">${escapeHtml(node.name)}</span></a></li>`;
115 };
116 return `<div class="sidebar-head"><input type="search" id="tree-filter" placeholder="Filter files…" autocomplete="off" spellcheck="false"><span class="tree-total">${root.fileCount} files</span></div><ul class="tree" id="tree">${renderNode(root, 0)}</ul>`;
117}
118
119function directoryListing(node, readmeHtml) {
120 const rows = node.children
121 .map((c) => {
122 if (c.type === 'dir') {
123 return `<tr><td class="col-icon"><span class="fi fi-dir">▸</span></td><td class="col-name"><a href="/files/${encodePath(c.path)}">${escapeHtml(c.name)}/</a></td><td class="col-meta">${c.fileCount} file${c.fileCount === 1 ? '' : 's'}</td><td class="col-size">${formatBytes(c.totalSize)}</td></tr>`;
124 }
125 return `<tr><td class="col-icon"><span class="fi fi-${escapeHtml(extOf(c.name) || 'none')}">${iconFor(c.name)}</span></td><td class="col-name"><a href="/files/${encodePath(c.path)}">${escapeHtml(c.name)}</a></td><td class="col-meta">${escapeHtml(extOf(c.name) || 'file')}</td><td class="col-size">${formatBytes(c.size)}</td></tr>`;
126 })
127 .join('');
128 const parent =
129 node.path !== ''
130 ? `<tr class="row-parent"><td class="col-icon">↰</td><td class="col-name"><a href="/files/${encodePath(node.path.split('/').slice(0, -1).join('/'))}">..</a></td><td></td><td></td></tr>`
131 : '';
132 return `
133<div class="file-head">
134 ${breadcrumbs(node.path, true)}
135 <div class="file-meta"><span>${node.children.filter((c) => c.type === 'dir').length} folders</span><span>${node.fileCount} files</span><span>${formatBytes(node.totalSize)}</span></div>
136</div>
137<div class="listing-wrap"><table class="listing"><tbody>${parent}${rows}</tbody></table></div>
138${readmeHtml ? `<section class="readme"><div class="readme-head">README.md</div><article class="markdown-body">${readmeHtml}</article></section>` : ''}`;
139}
140
141function fileHeader({ relPath, size, meta, actions }) {
142 const metaHtml = meta.map((m) => `<span>${m}</span>`).join('');
143 return `<div class="file-head">
144 ${breadcrumbs(relPath, false)}
145 <div class="file-meta">${metaHtml}<span>${formatBytes(size)}</span></div>
146 <div class="file-actions">${actions.join('')}</div>
147</div>`;
148}
149
150function tocAside(headings) {
151 if (!headings || headings.length < 2) return '';
152 const items = headings
153 .map((h) => `<li class="toc-l${h.level}"><a href="#${escapeHtml(h.id)}">${escapeHtml(h.text)}</a></li>`)
154 .join('');
155 return `<div class="toc"><div class="toc-title">On this page</div><ul>${items}</ul></div>`;
156}
157
158module.exports = { layout, breadcrumbs, sidebarTree, directoryListing, fileHeader, tocAside, SITE, extOf, iconFor };