Knowledge base: /docs/<title> permalinks, search, categories, count, better PDF

Articles get their own address (shareable, back/forward work); the list is
searchable by title and text, shows the article count, and is grouped into
Applications and Troubleshooting & Guides via a hidden category line set
from the editor. PDF export lays out at page width with page breaks, a title
block and page numbers; Print uses the browser's vector print. Inline code
is a pill that glows blue on hover, blockquotes no longer squeeze, and
GitHub-style [!NOTE] alerts render. [[Badge]] syntax inside code spans is
left alone.
This commit is contained in:
Johan Sandoval committed 2026-10-06 19:31:19 +00:00
1 parent 514bc31566
commit a5c0a5210e
24 files changed
+525 -11

No files matched your search

+1 -1
View File
@@ -1010,7 +1010,7 @@
if (!cfg.Demo) return realFetch(input, init);
let url;
try { url = new URL(typeof input === 'string' ? input : input.url, location.href); } catch (e) { return realFetch(input, init); }
try { url = new URL(typeof input === 'string' ? input : input.url, document.baseURI); } catch (e) { return realFetch(input, init); }
if (url.origin !== location.origin) return realFetch(input, init);
await Store.load();
+66
View File
@@ -0,0 +1,66 @@
/* ---------- Knowledge base ---------- */
/* search + count */
.kb-search { flex-shrink: 0; margin-bottom: 10px; }
.kb-search input { width: 100%; box-sizing: border-box; padding: 8px 10px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--input-bg); color: var(--text-main); font: inherit; font-size: 0.92em; outline: none; transition: border-color 0.15s, box-shadow 0.15s; }
.kb-search input:focus { border-color: #3498db; box-shadow: 0 0 0 3px rgba(52,152,219,0.25); }
.kb-count { color: var(--text-muted); font-size: 0.8em; margin-top: 6px; }
#docList { gap: 14px !important; }
.kb-empty { color: var(--text-muted); font-size: 0.9em; }
/* groups + items */
.kb-group-title { font-size: 0.72em; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted); margin: 2px 0 6px; display: flex; justify-content: space-between; }
.kb-group-title span { font-weight: 600; }
.kb-item { display: flex; align-items: flex-start; gap: 6px; padding: 6px 8px; margin: 0 -8px 2px; border-radius: 6px; border: 1px solid transparent; transition: background 0.15s, border-color 0.15s, box-shadow 0.15s; }
.kb-item:hover { background: rgba(52,152,219,0.08); border-color: rgba(52,152,219,0.45); box-shadow: 0 0 10px rgba(52,152,219,0.35); }
.kb-item.active { background: rgba(52,152,219,0.14); border-color: #3498db; }
.kb-item-main { min-width: 0; flex: 1; }
.kb-item a { color: #3498db; text-decoration: none; font-weight: 500; overflow-wrap: anywhere; }
.kb-item.active a { font-weight: 700; }
.kb-snip { font-size: 0.78em; color: var(--text-muted); margin-top: 2px; line-height: 1.35; overflow-wrap: anywhere; }
.kb-snip mark { background: rgba(52,152,219,0.3); color: inherit; border-radius: 2px; padding: 0 1px; }
.kb-copy { background: none; border: 0; padding: 2px; margin-top: 1px; color: var(--text-muted); opacity: 0.5; cursor: pointer; transition: opacity 0.15s, color 0.15s; }
.kb-copy:hover { opacity: 1; color: #3498db; }
.kb-copy svg { width: 14px; height: 14px; fill: currentColor; }
/* toolbar extras */
.kb-tool { padding: 6px 10px !important; border: 1px solid var(--border-color) !important; display: inline-flex !important; align-items: center; gap: 6px; }
.kb-tool svg { width: 14px; height: 14px; fill: currentColor; }
.kb-cat-select { padding: 6px 8px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--input-bg); color: var(--text-main); font: inherit; font-size: 0.85em; }
#docToolbar { flex-wrap: wrap; }
/* inline code: a pill that glows blue on hover (click to copy) */
#docContent :not(pre) > code {
font-family: Consolas, "Cascadia Mono", Menlo, monospace; font-size: 0.88em;
background: rgba(127,127,127,0.14); border: 1px solid var(--border-color); border-radius: 5px;
padding: 1px 6px; color: var(--text-main); cursor: copy; white-space: break-spaces; overflow-wrap: anywhere;
transition: border-color 0.15s, box-shadow 0.15s, color 0.15s, background 0.15s;
}
#docContent :not(pre) > code:hover {
border-color: #3498db; color: #3498db; background: rgba(52,152,219,0.1);
box-shadow: 0 0 0 1px rgba(52,152,219,0.35), 0 0 12px rgba(52,152,219,0.55);
}
[data-theme="dark"] #docContent :not(pre) > code:hover { color: #85c1e9; }
/* blockquotes: normal block flow (multi-line quotes, lists and links no longer squeeze) */
#docContent blockquote { display: block !important; padding: 10px 16px !important; min-height: 0 !important; line-height: 1.6; }
#docContent blockquote p { display: block !important; margin: 0 0 8px !important; }
#docContent blockquote > :last-child { margin-bottom: 0 !important; }
#docContent blockquote ul, #docContent blockquote ol { margin: 4px 0; padding-left: 22px; }
/* GitHub-style alerts: > [!NOTE] */
#docContent blockquote.kb-alert { border-left-width: 4px; }
.kb-alert-title { font-weight: 700; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; }
#docContent .kb-alert-note { border-left-color: #2a78d6 !important; } .kb-alert-note .kb-alert-title { color: #2a78d6; }
#docContent .kb-alert-tip { border-left-color: #0ca30c !important; } .kb-alert-tip .kb-alert-title { color: #0a8a0a; }
#docContent .kb-alert-important { border-left-color: #8250df !important; } .kb-alert-important .kb-alert-title { color: #8250df; }
#docContent .kb-alert-warning { border-left-color: #d29922 !important; } .kb-alert-warning .kb-alert-title { color: #9a6700; }
#docContent .kb-alert-caution { border-left-color: #d03b3b !important; } .kb-alert-caution .kb-alert-title { color: #d03b3b; }
[data-theme="dark"] .kb-alert-tip .kb-alert-title { color: #3fb950; }
[data-theme="dark"] .kb-alert-important .kb-alert-title { color: #a371f7; }
[data-theme="dark"] .kb-alert-warning .kb-alert-title { color: #d29922; }
[data-theme="dark"] .kb-alert-caution .kb-alert-title { color: #f85149; }
[data-theme="dark"] .kb-alert-note .kb-alert-title { color: #58a6ff; }
@media (max-width: 1100px) {
.kb-tool span { display: none; }
}
+411
View File
@@ -0,0 +1,411 @@
/*
* USMT Dash - knowledge base upgrades
*
* - Every article has its own address: /docs/<title> (shareable; back/forward work).
* - The article list is searchable (titles and full text), shows how many articles there
* are, and is split into Applications and Troubleshooting & Guides. An article's group
* comes from a hidden first line, <!-- category: Applications -->, set from the editor;
* articles without one are grouped by a heuristic (named after an app = Applications).
* - PDF export lays the article out at page width with real page breaks, a title block
* and page numbers. Print uses the browser's own (vector) print-to-PDF.
* - GitHub-style alerts in blockquotes: > [!NOTE], [!TIP], [!IMPORTANT], [!WARNING], [!CAUTION].
*/
(function () {
'use strict';
const CATS = ['Applications', 'Troubleshooting & Guides'];
const CAT_RE = /^\s*<!--\s*category:\s*([^>]*?)\s*-->[ \t]*\r?\n?/i;
const index = {}; // title -> { text, lower, category }
let query = '';
let indexing = null;
let firstLoad = true;
const esc = s => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
const docs = () => (typeof availableDocs !== 'undefined' && Array.isArray(availableDocs)) ? availableDocs : [];
const permalink = t => '/docs/' + encodeURIComponent(t);
const currentTitle = () => (typeof currentDocTitle !== 'undefined' ? currentDocTitle : '');
function categoryOf(title, text) {
const m = String(text || '').match(CAT_RE);
if (m && m[1]) return m[1].trim();
// Heuristic: named after software we've seen on devices, or written as an install guide
try {
if (typeof softwareCache !== 'undefined') {
const t = title.toLowerCase();
for (const list of Object.values(softwareCache)) {
if (Array.isArray(list) && list.some(a => a && a.Name && getAppDisplayName(a.Name).toLowerCase() === t)) return CATS[0];
}
}
} catch (e) {}
if (/^#{1,3}\s*(install|installation|deploy)/im.test(text || '')) return CATS[0];
return CATS[1];
}
async function buildIndex() {
const titles = docs();
await Promise.all(titles.map(async t => {
try {
const r = await fetch(`/Documentation/${encodeURIComponent(t)}.md?t=${Date.now()}`);
const text = r.ok ? await r.text() : '';
index[t] = { text, lower: text.toLowerCase(), category: categoryOf(t, text) };
} catch (e) { index[t] = { text: '', lower: '', category: categoryOf(t, '') }; }
}));
Object.keys(index).forEach(t => { if (!titles.includes(t)) delete index[t]; });
}
function reindex() {
indexing = buildIndex().then(() => { indexing = null; renderSidebar(); });
return indexing;
}
// ---------------------------------------------------------------------------------
// Sidebar: search, count, categories
// ---------------------------------------------------------------------------------
const LINK_SVG = '<svg class="icon-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M451.5 160C434.9 160 418.8 164.5 404.7 172.7C388.9 156.7 370.5 143.3 350.2 133.2C378.4 109.2 414.3 96 451.5 96C537.9 96 608 166 608 252.5C608 294 591.5 333.8 562.2 363.1L491.1 434.2C461.8 463.5 422 480 380.5 480C294.1 480 224 410 224 323.5C224 322 224 320.5 224.1 319C224.6 301.3 239.3 287.4 257 287.9C274.7 288.4 288.6 303.1 288.1 320.8C288.1 321.7 288.1 322.6 288.1 323.4C288.1 374.5 329.5 415.9 380.6 415.9C405.1 415.9 428.6 406.2 446 388.8L517.1 317.7C534.4 300.4 544.2 276.8 544.2 252.3C544.2 201.2 502.8 159.8 451.7 159.8zM307.2 237.3C305.3 236.5 303.4 235.4 301.7 234.2C289.1 227.7 274.7 224 259.6 224C235.1 224 211.6 233.7 194.2 251.1L123.1 322.2C105.8 339.5 96 363.1 96 387.6C96 438.7 137.4 480.1 188.5 480.1C205 480.1 221.1 475.7 235.2 467.5C251 483.5 269.4 496.9 289.8 507C261.6 530.9 225.8 544.2 188.5 544.2C102.1 544.2 32 474.2 32 387.7C32 346.2 48.5 306.4 77.8 277.1L148.9 206C178.2 176.7 218 160.2 259.5 160.2C346.1 160.2 416 230.8 416 317.1C416 318.4 416 319.7 416 321C415.6 338.7 400.9 352.6 383.2 352.2C365.5 351.8 351.6 337.1 352 319.4C352 318.6 352 317.9 352 317.1C352 283.4 334 253.8 307.2 237.5z"/></svg>';
function ensureSearchUi() {
const list = document.getElementById('docList');
if (!list || document.getElementById('docSearch')) return;
const box = document.createElement('div');
box.className = 'kb-search';
box.innerHTML = `
<input type="search" id="docSearch" placeholder="Search articles..." autocomplete="off" aria-label="Search articles">
<div class="kb-count" id="docCount" aria-live="polite"></div>`;
list.parentElement.insertBefore(box, list);
const input = box.querySelector('input');
input.addEventListener('input', () => { query = input.value.trim(); renderSidebar(); });
input.addEventListener('keydown', e => {
if (e.key === 'Enter') { const first = list.querySelector('.kb-item a'); if (first) first.click(); }
if (e.key === 'Escape') { input.value = ''; query = ''; renderSidebar(); }
});
}
function snippet(text, q) {
const body = text.replace(CAT_RE, '').replace(/\[\[(\w+):([^\]|]+)(\|[^\]]*)?\]\]/g, '$2').replace(/[#>*_`]/g, '').replace(/\s+/g, ' ');
const i = body.toLowerCase().indexOf(q.toLowerCase());
if (i < 0) return '';
const start = Math.max(0, i - 40), end = Math.min(body.length, i + q.length + 60);
return (start ? '...' : '') + esc(body.slice(start, i)) + '<mark>' + esc(body.slice(i, i + q.length)) + '</mark>' + esc(body.slice(i + q.length, end)) + (end < body.length ? '...' : '');
}
function renderSidebar() {
ensureSearchUi();
const container = document.getElementById('docList');
if (!container) return;
const all = docs();
const count = document.getElementById('docCount');
if (!all.length) {
container.innerHTML = "<span class='kb-empty'>No articles yet. Use + Add to write the first one.</span>";
if (count) count.textContent = '0 articles';
return;
}
const q = query.toLowerCase();
const hits = all.map(t => {
const e = index[t];
const inTitle = !q || t.toLowerCase().includes(q);
const inText = q && e && e.lower.includes(q);
if (!inTitle && !inText) return null;
return { t, cat: e ? e.category : null, snip: (!inTitle && inText) ? snippet(e.text, query) : '' };
}).filter(Boolean);
if (count) {
count.textContent = q ? `${hits.length} of ${all.length} article${all.length === 1 ? '' : 's'} match` : `${all.length} article${all.length === 1 ? '' : 's'}`;
if (q && indexing) count.textContent += ' (searching text...)';
}
const groups = {};
hits.forEach(h => { const c = h.cat || 'Articles'; (groups[c] = groups[c] || []).push(h); });
const order = Object.keys(groups).sort((a, b) => {
const ia = CATS.indexOf(a), ib = CATS.indexOf(b);
return (ia < 0 ? 99 : ia) - (ib < 0 ? 99 : ib) || a.localeCompare(b);
});
const active = currentTitle();
container.innerHTML = hits.length ? order.map(cat => `
<div class="kb-group">
<div class="kb-group-title">${esc(cat)} <span>${groups[cat].length}</span></div>
${groups[cat].map(h => `
<div class="kb-item${h.t === active ? ' active' : ''}">
<button type="button" class="kb-copy" data-copy-doc="${esc(h.t)}" title="Copy a Markdown link to this article">${LINK_SVG}</button>
<div class="kb-item-main">
<a href="${permalink(h.t)}" data-doc="${esc(h.t)}">${esc(h.t)}</a>
${h.snip ? `<div class="kb-snip">${h.snip}</div>` : ''}
</div>
</div>`).join('')}
</div>`).join('') : `<span class="kb-empty">No article mentions "${esc(query)}".</span>`;
}
// Clicks in the list: open in place (middle / ctrl-click still open a new tab)
document.addEventListener('click', e => {
const a = e.target.closest('#docList a[data-doc]');
if (a && !e.ctrlKey && !e.metaKey && !e.shiftKey && e.button === 0) { e.preventDefault(); openDoc(a.dataset.doc); return; }
// Links between articles (/docs/Title or the older ?doc=Title) open in place
const link = e.target.closest('#docContent a[href]');
if (link && !e.ctrlKey && !e.metaKey && !e.shiftKey && e.button === 0 && link.target !== '_blank') {
let target = null;
try {
const u = new URL(link.getAttribute('href'), document.baseURI);
if (u.origin === location.origin) {
const m = u.pathname.match(/^\/docs\/(.+)$/);
target = m ? decodeURIComponent(m[1]) : u.searchParams.get('doc');
}
} catch (err) {}
const doc = findDoc(target);
if (doc) { e.preventDefault(); openDoc(doc); return; }
}
const c = e.target.closest('#docList .kb-copy');
if (c) { e.preventDefault(); copyDocLink(c.dataset.copyDoc); }
});
// ---------------------------------------------------------------------------------
// Permalinks
// ---------------------------------------------------------------------------------
function titleFromPath() {
const m = location.pathname.match(/^\/docs\/(.+)$/);
if (!m) return null;
try { return decodeURIComponent(m[1]).replace(/\/+$/, ''); } catch (e) { return null; }
}
function findDoc(name) {
if (!name) return null;
const all = docs();
return all.find(d => d === name) || all.find(d => d.toLowerCase() === name.toLowerCase()) || null;
}
function setUrl(path, push) {
if (location.pathname + location.search === path) return;
try { history[push ? 'pushState' : 'replaceState']({ doc: path }, '', path); } catch (e) {}
}
function copyPageLink() {
const t = currentTitle();
if (!t) return;
fallbackCopyTextToClipboard(location.origin + permalink(t));
showToast('Page link copied to clipboard!');
}
function addToolbarButtons() {
const bar = document.getElementById('docToolbar');
if (!bar || document.getElementById('btn-copy-page')) return;
const pdf = document.getElementById('btn-download-pdf');
const mk = (id, title, html, fn) => {
const b = document.createElement('button');
b.className = 'nav-btn kb-tool'; b.id = id; b.title = title; b.type = 'button'; b.innerHTML = html; b.onclick = fn;
bar.insertBefore(b, pdf);
return b;
};
mk('btn-copy-page', 'Copy a link to this page', `${LINK_SVG}<span>Link</span>`, copyPageLink);
mk('btn-print-doc', 'Print, or save as PDF with your browser', '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 1h8v4H4zM2 6h12a1 1 0 0 1 1 1v5h-3v3H4v-3H1V7a1 1 0 0 1 1-1zm3 6v2h6v-2zm8-4a.8.8 0 1 0 0 1.6A.8.8 0 0 0 13 8z" fill="currentColor"/></svg><span>Print</span>', printDoc);
// Category picker (edit mode only)
const sel = document.createElement('select');
sel.id = 'docCategory'; sel.className = 'kb-cat-select'; sel.title = 'Which group this article is listed under';
sel.innerHTML = CATS.map(c => `<option>${esc(c)}</option>`).join('');
sel.style.display = 'none';
sel.onchange = () => {
const ed = document.getElementById('docEditor');
ed.value = `<!-- category: ${sel.value} -->\n` + ed.value.replace(CAT_RE, '');
};
bar.insertBefore(sel, document.getElementById('btn-save-doc'));
}
// ---------------------------------------------------------------------------------
// PDF + print
// ---------------------------------------------------------------------------------
const PRINT_CSS = `
.kb-print { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; color: #1f2328; background: #fff; font-size: 12.5px; line-height: 1.55; }
.kb-print * { box-sizing: border-box; }
.kb-print .kb-print-head { border-bottom: 2px solid #2a78d6; padding-bottom: 8px; margin-bottom: 14px; }
.kb-print .kb-print-head h1 { font-size: 22px; margin: 0; border: 0; padding: 0; color: #1f2328; }
.kb-print .kb-print-head div { color: #59636e; font-size: 11px; margin-top: 3px; }
.kb-print h1, .kb-print h2 { border-bottom: 1px solid #d1d9e0; padding-bottom: 4px; color: #1f2328; }
.kb-print h1 { font-size: 19px; } .kb-print h2 { font-size: 16px; } .kb-print h3 { font-size: 14px; }
.kb-print h1, .kb-print h2, .kb-print h3, .kb-print h4 { break-after: avoid; page-break-after: avoid; margin: 14px 0 8px; }
.kb-print p, .kb-print li { orphans: 3; widows: 3; }
.kb-print pre, .kb-print table, .kb-print blockquote, .kb-print img, .kb-print .kb-code { break-inside: avoid; page-break-inside: avoid; }
.kb-print a { color: #0969da; text-decoration: none; font-weight: 600; }
.kb-print code { font-family: Consolas, "Cascadia Mono", Menlo, monospace; font-size: 0.88em; background: #eff2f5; border: 1px solid #d1d9e0; border-radius: 4px; padding: 1px 5px; color: #1f2328; }
.kb-print pre { background: #f6f8fa !important; border: 1px solid #d1d9e0; border-radius: 6px; padding: 10px 12px !important; white-space: pre-wrap; word-break: break-word; margin: 8px 0 !important; }
.kb-print pre code, .kb-print pre code * { background: none !important; border: 0; padding: 0; color: #1f2328 !important; }
.kb-print .kb-code-head, .kb-print .copy-code-btn, .kb-print button { display: none !important; }
.kb-print table { border-collapse: collapse; width: 100%; margin: 8px 0; font-size: 0.95em; }
.kb-print th, .kb-print td { border: 1px solid #d1d9e0; padding: 5px 8px; text-align: left; vertical-align: top; }
.kb-print th { background: #eef4fc !important; color: #1f2328 !important; font-weight: 700; }
.kb-print td { background: #fff !important; color: #1f2328 !important; }
.kb-print th, .kb-print td { white-space: normal !important; overflow: visible !important; text-overflow: clip !important; max-width: none !important; position: static !important; }
.kb-print p, .kb-print li, .kb-print h1, .kb-print h2, .kb-print h3, .kb-print h4, .kb-print strong, .kb-print em { color: #1f2328; }
.kb-print blockquote { display: block !important; margin: 8px 0; padding: 8px 12px; border-left: 4px solid #2a78d6; background: #f3f7fc; color: #1f2328; border-radius: 0 4px 4px 0; }
.kb-print blockquote p { display: block !important; margin: 0 0 6px; }
.kb-print blockquote p:last-child { margin-bottom: 0; }
.kb-print img { max-width: 100% !important; max-height: 420px; height: auto !important; display: block; margin: 8px 0; border: 1px solid #d1d9e0; border-radius: 4px; }
.kb-print .tag-badge { border: 1px solid currentColor; }
.kb-print mark { background: none; }`;
function printableClone() {
const content = document.getElementById('docContent');
const wrap = document.createElement('div');
wrap.className = 'kb-print';
const st = document.createElement('style'); st.textContent = PRINT_CSS; wrap.appendChild(st);
const head = document.createElement('div'); head.className = 'kb-print-head';
const date = document.getElementById('docEditDate');
const body = content.cloneNode(true);
// The article usually opens with its own H1; don't print the title twice
const firstH1 = body.querySelector('h1');
const dup = firstH1 && firstH1.textContent.trim().toLowerCase() === currentTitle().trim().toLowerCase();
head.innerHTML = `${dup ? '' : `<h1>${esc(currentTitle())}</h1>`}<div>${esc(date ? date.textContent : '')}${date ? ' &middot; ' : ''}${esc(location.origin + '/docs/' + currentTitle())}</div>`;
wrap.appendChild(head);
body.removeAttribute('id'); body.removeAttribute('style');
// Undo on-screen code block chrome (language bar + copy button container)
body.querySelectorAll('pre').forEach(pre => { pre.removeAttribute('style'); });
body.querySelectorAll('[style]').forEach(n => { if (n.tagName !== 'SPAN' || !n.classList.contains('tag-badge')) n.style.removeProperty('background'); });
wrap.appendChild(body);
return wrap;
}
function waitForImages(root) {
return Promise.all(Array.from(root.querySelectorAll('img')).map(img => img.complete ? null : new Promise(r => { img.onload = img.onerror = r; })));
}
async function downloadPdf() {
if (!currentTitle()) return;
const btn = document.getElementById('btn-download-pdf');
const label = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = label.replace('PDF', 'Exporting...');
// Lay the article out offscreen at the printable width of an A4/Letter page
// (images load while it's briefly attached and hidden, then html2pdf takes its own copy)
const host = document.createElement('div');
host.style.cssText = 'position: fixed; left: 0; top: 0; width: 688px; visibility: hidden; pointer-events: none;';
const page = printableClone();
page.style.width = '688px';
host.appendChild(page);
document.body.appendChild(host);
try {
await waitForImages(page);
host.removeChild(page);
const opt = {
margin: [14, 14, 16, 14],
filename: `${currentTitle()}.pdf`,
image: { type: 'jpeg', quality: 0.95 },
html2canvas: { scale: 2, useCORS: true, logging: false, backgroundColor: '#ffffff', scrollX: 0, scrollY: 0 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
pagebreak: { mode: ['css', 'legacy'], avoid: ['pre', 'table', 'img', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'li', 'tr'] }
};
await html2pdf().set(opt).from(page).toPdf().get('pdf').then(pdf => {
const n = pdf.internal.getNumberOfPages();
const w = pdf.internal.pageSize.getWidth(), h = pdf.internal.pageSize.getHeight();
pdf.setFontSize(8); pdf.setTextColor(120);
for (let i = 1; i <= n; i++) {
pdf.setPage(i);
pdf.text(currentTitle(), 14, h - 7);
pdf.text(`Page ${i} of ${n}`, w - 14, h - 7, { align: 'right' });
}
}).save();
} catch (e) {
showToast('PDF export failed. Try Print > Save as PDF.');
} finally {
host.remove();
btn.disabled = false;
btn.innerHTML = label;
}
}
function printDoc() {
if (!currentTitle()) return;
const w = window.open('', '_blank');
if (!w) { showToast('Allow pop-ups to print this article.'); return; }
const page = printableClone();
w.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>${esc(currentTitle())}</title><base href="${esc(location.origin)}/">
<style>@page { margin: 16mm 14mm; } body { margin: 0; background: #fff; } .kb-print { max-width: 760px; margin: 0 auto; }</style></head><body></body></html>`);
w.document.body.appendChild(w.document.importNode(page, true));
w.document.close();
const go = () => { w.focus(); w.print(); };
Promise.all(Array.from(w.document.images).map(i => i.complete ? null : new Promise(r => { i.onload = i.onerror = r; }))).then(() => setTimeout(go, 150));
}
// ---------------------------------------------------------------------------------
// Rendering extras: GitHub-style alerts
// ---------------------------------------------------------------------------------
const ALERTS = { NOTE: 'Note', TIP: 'Tip', IMPORTANT: 'Important', WARNING: 'Warning', CAUTION: 'Caution' };
function decorate() {
document.querySelectorAll('#docContent blockquote').forEach(bq => {
if (bq.dataset.kb) return;
bq.dataset.kb = '1';
const p = bq.querySelector('p');
if (!p) return;
const m = p.innerHTML.match(/^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*(<br>)?\s*/i);
if (!m) return;
const kind = m[1].toUpperCase();
p.innerHTML = p.innerHTML.slice(m[0].length);
if (!p.innerHTML.trim()) p.remove();
bq.classList.add('kb-alert', 'kb-alert-' + kind.toLowerCase());
const t = document.createElement('div');
t.className = 'kb-alert-title';
t.textContent = ALERTS[kind];
bq.insertBefore(t, bq.firstChild);
});
}
// ---------------------------------------------------------------------------------
// Wiring
// ---------------------------------------------------------------------------------
function wrap(name, after, before) {
const orig = window[name];
if (typeof orig !== 'function') return;
window[name] = function () {
if (before) before.apply(this, arguments);
const r = orig.apply(this, arguments);
if (r && typeof r.then === 'function') return r.then(v => { after && after.apply(this, arguments); return v; });
if (after) after.apply(this, arguments);
return r;
};
}
document.addEventListener('DOMContentLoaded', () => {
window.renderDocsSidebar = renderSidebar;
window.downloadDocAsPDF = downloadPdf;
// Markdown link for use inside other articles
window.copyDocLink = function (docName) {
fallbackCopyTextToClipboard(`[${docName}](${permalink(docName)})`);
showToast('Markdown link copied: paste it into another article.');
};
addToolbarButtons();
wrap('loadDocsList', () => {
reindex();
if (firstLoad) {
firstLoad = false;
const t = titleFromPath();
if (location.pathname.startsWith('/docs')) {
const doc = findDoc(t);
if (doc) openDoc(doc);
else {
switchPage('docs');
if (t) {
const c = document.getElementById('docContent');
if (c) c.innerHTML = `<span style="color:#e74c3c; font-weight:bold;">There's no article called "${esc(t)}".</span> Pick one from the list, or search for it.`;
}
}
}
}
});
wrap('openDoc', () => { renderSidebar(); decorate(); }, name => { setUrl(permalink(name), true); });
wrap('formatCodeBlocks', decorate);
wrap('setDocMode', mode => {
const sel = document.getElementById('docCategory');
if (!sel) return;
sel.style.display = mode === 'edit' ? '' : 'none';
if (mode === 'edit') {
const ed = document.getElementById('docEditor');
const m = ed.value.match(CAT_RE);
sel.value = m && CATS.includes(m[1].trim()) ? m[1].trim() : categoryOf(currentTitle(), ed.value);
if (!m) ed.value = `<!-- category: ${sel.value} -->\n` + ed.value;
}
});
wrap('switchPage', id => {
if (id !== 'docs' && location.pathname.startsWith('/docs')) setUrl('/', false);
if (id === 'docs' && currentTitle() && !location.pathname.startsWith('/docs/')) setUrl(permalink(currentTitle()), false);
});
window.addEventListener('popstate', () => {
const doc = findDoc(titleFromPath());
if (doc) openDoc(doc);
else if (location.pathname === '/' && document.getElementById('docs').style.display !== 'none') switchPage('overview');
});
});
})();
+24 -10
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>USMT Dash</title>
<base href="/">
<link rel="icon" href="/favicon.png" type="image/png">
<script src="/vendor/marked/marked.min.js"></script>
@@ -353,9 +354,11 @@
</style>
<link rel="stylesheet" href="/layout.css">
<link rel="stylesheet" href="/overview.css">
<link rel="stylesheet" href="/docs.css">
<script src="/layout.js"></script>
<script src="/device-info.js"></script>
<script src="/overview.js"></script>
<script src="/docs.js"></script>
<script src="/excel-filter.js" defer></script>
</head>
<body>
@@ -1751,7 +1754,7 @@
const preview = document.getElementById('docContent');
if (preview && preview.style.display !== 'none') {
// Encode spaces ONLY in UNC/Local paths so marked.js doesn't fail to parse them
let safeText = content.replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
let safeText = shieldCode(content).replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
return `[${title}](${url.replace(/ /g, '%20')})`;
});
@@ -1772,7 +1775,7 @@
return `<a href="javascript:void(0);" class="copy-trigger" data-copy="${safeVal}" title="Click to copy">${safeDisplay}</a>`;
});
preview.innerHTML = typeof marked !== 'undefined' ? marked.parse(safeText) : `<pre>${safeText}</pre>`;
preview.innerHTML = typeof marked !== 'undefined' ? marked.parse(unshieldCode(safeText)) : `<pre>${unshieldCode(safeText)}</pre>`;
formatCodeBlocks();
}
}
@@ -1826,6 +1829,17 @@
startDashboard();
};
// Code spans and fenced blocks are set aside while the [[Badge:..]] / [[Copy:..]] / UNC
// path rewrites run, so an article can show that syntax literally inside `code`
const CODE_SHIELD = [];
function shieldCode(text) {
CODE_SHIELD.length = 0;
return String(text).replace(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]+`)/g, m => `\u0000CODE${CODE_SHIELD.push(m) - 1}\u0000`);
}
function unshieldCode(text) {
return String(text).replace(/\u0000CODE(\d+)\u0000/g, (m, i) => CODE_SHIELD[+i]);
}
async function loadDocsList() {
try {
const res = await fetch('/get-docs-list?t=' + Date.now());
@@ -1890,7 +1904,7 @@
`;
// Encode spaces ONLY in UNC/Local paths so marked.js doesn't fail to parse them
let safeText = text.replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
let safeText = shieldCode(text).replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
return `[${title}](${url.replace(/ /g, '%20')})`;
});
@@ -1911,7 +1925,7 @@
return `<a href="javascript:void(0);" class="copy-trigger" data-copy="${safeVal}" title="Click to copy">${safeDisplay}</a>`;
});
const parsedContent = typeof marked !== 'undefined' ? marked.parse(safeText) : `<pre style="white-space: pre-wrap; font-family: inherit;">${safeText}</pre>`;
const parsedContent = typeof marked !== 'undefined' ? marked.parse(unshieldCode(safeText)) : `<pre style="white-space: pre-wrap; font-family: inherit;">${unshieldCode(safeText)}</pre>`;
document.getElementById('docContent').innerHTML = parsedContent;
// Keep the actual spaces in the editor so the user doesn't see ugly %20 encoding!
@@ -1975,7 +1989,7 @@
const rawText = editor.value;
if (rawText) {
// Encode spaces ONLY in UNC/Local paths so marked.js doesn't fail to parse them
let safeText = rawText.replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
let safeText = shieldCode(rawText).replace(/\[([^\]]+)\]\(((\\\\|[A-Za-z]:\\)[^\)]+)\)/g, (match, title, url) => {
return `[${title}](${url.replace(/ /g, '%20')})`;
});
@@ -1996,7 +2010,7 @@
return `<a href="javascript:void(0);" class="copy-trigger" data-copy="${safeVal}" title="Click to copy">${safeDisplay}</a>`;
});
preview.innerHTML = typeof marked !== 'undefined' ? marked.parse(safeText) : `<pre>${safeText}</pre>`;
preview.innerHTML = typeof marked !== 'undefined' ? marked.parse(unshieldCode(safeText)) : `<pre>${unshieldCode(safeText)}</pre>`;
formatCodeBlocks();
}
}
@@ -2688,7 +2702,7 @@
const nameText = s.Name || '--';
const linkMatch = availableDocs.find(d => d.toLowerCase() === nameText.toLowerCase());
const nameDisplay = linkMatch
? `<a href="?doc=${encodeURIComponent(linkMatch)}" target="_blank" onclick="event.stopPropagation();" style="color: #3498db;"><b>${nameText}</b></a>`
? `<a href="/docs/${encodeURIComponent(linkMatch)}" target="_blank" onclick="event.stopPropagation();" style="color: #3498db;"><b>${nameText}</b></a>`
: `<b>${nameText}</b>`;
return `
<tr>
@@ -2833,7 +2847,7 @@
let hyperlinkedApps = sApps.map(app => {
const linkMatch = availableDocs.find(d => d.toLowerCase() === app.toLowerCase());
return linkMatch
? `<a href="?doc=${encodeURIComponent(linkMatch)}" target="_blank" onclick="event.stopPropagation();" style="color: #3498db; font-weight: bold;">${app}</a>`
? `<a href="/docs/${encodeURIComponent(linkMatch)}" target="_blank" onclick="event.stopPropagation();" style="color: #3498db; font-weight: bold;">${app}</a>`
: app;
});
specialtySoftware = hyperlinkedApps.length > 0 ? hyperlinkedApps.join("<br>") : "None";
@@ -3058,7 +3072,7 @@
let hyperlinkedMissing = missing.map(app => {
const linkMatch = availableDocs.find(d => d.toLowerCase() === app.toLowerCase());
return linkMatch
? `<a href="?doc=${encodeURIComponent(linkMatch)}" target="_blank" onclick="event.stopPropagation();" style="color: #3498db; font-weight: bold;">${app}</a>`
? `<a href="/docs/${encodeURIComponent(linkMatch)}" target="_blank" onclick="event.stopPropagation();" style="color: #3498db; font-weight: bold;">${app}</a>`
: app;
});
missingSoftwareDisplay = hyperlinkedMissing.length > 0 ? hyperlinkedMissing.join(", ") : "None";
@@ -3171,7 +3185,7 @@
let hyperlinkedMissing = missing.map(app => {
const linkMatch = availableDocs.find(d => d.toLowerCase() === app.toLowerCase());
return linkMatch
? `<a href="?doc=${encodeURIComponent(linkMatch)}" target="_blank" onclick="event.stopPropagation();" style="color: #3498db; font-weight: bold;">${app}</a>`
? `<a href="/docs/${encodeURIComponent(linkMatch)}" target="_blank" onclick="event.stopPropagation();" style="color: #3498db; font-weight: bold;">${app}</a>`
: app;
});
enriched["MissingSoftwareDisplay"] = hyperlinkedMissing.length > 0 ? hyperlinkedMissing.join(", ") : "None";