pboProperty Ontology
javascript99 lines5.0 KB
RawDownload
1'use strict';
2
3// Injects the site navigation bar into the standalone generated documents
4// (board report, semantic atlas) when they are served at /report and /atlas.
5// The source HTML files are left untouched; the bar is added in memory.
6// Behaviour: fixed at the top, slides away when scrolling down, returns when
7// scrolling up or when the pointer reaches the top edge.
8
9const fsp = require('fs/promises');
10
11const NAV_HEIGHT = 44;
12
13function navMarkup(active) {
14 const links = [
15 ['/', 'Home', 'home'],
16 ['/report', 'Board report', 'report'],
17 ['/atlas', 'Semantic atlas', 'atlas'],
18 ['/files', 'Files', 'files'],
19 ]
20 .map(([href, label, key]) => `<a href="${href}"${key === active ? ' class="pbo-active"' : ''}>${label}</a>`)
21 .join('');
22
23 return `
24<style id="pbo-docnav-style">
25 #pbo-docnav{position:fixed;top:0;left:0;right:0;height:${NAV_HEIGHT}px;z-index:2147483000;display:flex;align-items:center;gap:14px;padding:0 16px;background:#1a2332;color:#f1faee;border-bottom:1px solid rgba(255,255,255,.08);font:14px/1 "Inter","Segoe UI",system-ui,-apple-system,Helvetica,Arial,sans-serif;box-shadow:0 2px 12px rgba(0,0,0,.18);transition:transform .22s ease,opacity .22s ease;will-change:transform}
26 #pbo-docnav.pbo-hidden{transform:translateY(-100%);opacity:0;pointer-events:none}
27 #pbo-docnav .pbo-brand{display:inline-flex;align-items:center;gap:9px;color:#f1faee;font-weight:600;text-decoration:none;margin-right:6px}
28 #pbo-docnav .pbo-mark{font:600 11px/1 ui-monospace,Menlo,Consolas,monospace;letter-spacing:.08em;background:#2d8b8b;color:#fff;padding:4px 7px;border-radius:5px}
29 #pbo-docnav nav{display:flex;gap:2px}
30 #pbo-docnav nav a{color:#a8dadc;text-decoration:none;padding:7px 10px;border-radius:6px;font-size:13.5px}
31 #pbo-docnav nav a:hover{background:rgba(255,255,255,.08);color:#fff}
32 #pbo-docnav nav a.pbo-active{background:rgba(255,255,255,.14);color:#fff}
33 #pbo-docnav .pbo-right{margin-left:auto;display:flex;gap:8px;align-items:center}
34 #pbo-docnav .pbo-right a{color:#a8dadc;text-decoration:none;font-size:12.5px;border:1px solid rgba(255,255,255,.14);padding:5px 9px;border-radius:6px}
35 #pbo-docnav .pbo-right a:hover{background:rgba(255,255,255,.1);color:#fff}
36 body{margin-top:${NAV_HEIGHT}px!important}
37 #pbo-docnav-hotzone{position:fixed;top:0;left:0;right:0;height:8px;z-index:2147482999}
38 @media (max-width:640px){#pbo-docnav .pbo-brand span:last-child{display:none}#pbo-docnav .pbo-right{display:none}}
39 @media print{#pbo-docnav,#pbo-docnav-hotzone{display:none!important}body{margin-top:0!important}}
40 /* The semantic atlas pins a render-status badge to the top-right; keep it visible below the bar. */
41 body > #status{top:${NAV_HEIGHT}px!important}
42</style>
43<div id="pbo-docnav-hotzone" aria-hidden="true"></div>
44<header id="pbo-docnav">
45 <a class="pbo-brand" href="/"><span class="pbo-mark">pbo</span><span>Property Ontology</span></a>
46 <nav aria-label="Site">${links}</nav>
47 <div class="pbo-right"><a href="?raw" title="Open the document without the navigation bar">Raw document ↗</a></div>
48</header>
49<script id="pbo-docnav-script">
50(function(){
51 var bar=document.getElementById('pbo-docnav');
52 var hot=document.getElementById('pbo-docnav-hotzone');
53 if(!bar)return;
54 var last=window.pageYOffset||document.documentElement.scrollTop||0;
55 var ticking=false,hidden=false;
56 var THRESH=6,MIN=${NAV_HEIGHT + 20};
57 function set(h){if(h===hidden)return;hidden=h;bar.classList.toggle('pbo-hidden',h);}
58 function onScroll(){
59 var y=window.pageYOffset||document.documentElement.scrollTop||0;
60 var d=y-last;
61 if(y<=MIN){set(false);}
62 else if(d>THRESH){set(true);}
63 else if(d<-THRESH){set(false);}
64 if(Math.abs(d)>THRESH)last=y;
65 ticking=false;
66 }
67 window.addEventListener('scroll',function(){if(!ticking){ticking=true;requestAnimationFrame(onScroll);}},{passive:true});
68 if(hot){hot.addEventListener('mouseenter',function(){set(false);});}
69 bar.addEventListener('mouseenter',function(){set(false);});
70 document.addEventListener('keydown',function(e){if(e.key==='Escape')set(false);});
71 window.addEventListener('hashchange',function(){last=window.pageYOffset||0;});
72})();
73</script>`;
74}
75
76const cache = new Map(); // abs path -> { mtimeMs, html }
77
78/**
79 * Read a standalone HTML document and return it with the nav bar injected
80 * right after the opening <body> tag. Cached by file mtime.
81 */
82async function withNav(absPath, active) {
83 const st = await fsp.stat(absPath);
84 const key = `${absPath}::${active}`;
85 const hit = cache.get(key);
86 if (hit && hit.mtimeMs === st.mtimeMs) return hit.html;
87
88 const src = await fsp.readFile(absPath, 'utf8');
89 const snippet = navMarkup(active);
90 // Insert before the closing </body>. Generated documents inline large JS
91 // bundles in <head> that contain literal "<body>" strings, so matching the
92 // opening tag is unsafe; the final </body> is always the real one.
93 const idx = src.lastIndexOf('</body>');
94 const html = idx !== -1 ? src.slice(0, idx) + snippet + src.slice(idx) : src + snippet;
95 cache.set(key, { mtimeMs: st.mtimeMs, html });
96 return html;
97}
98
99module.exports = { withNav, NAV_HEIGHT };