/*
* USMT Dash - knowledge base upgrades
*
* - Every article has its own address: /docs/
(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, , 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*[ \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 => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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 = '';
function ensureSearchUi() {
const list = document.getElementById('docList');
if (!list || document.getElementById('docSearch')) return;
const box = document.createElement('div');
box.className = 'kb-search';
box.innerHTML = `
`;
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)) + '' + esc(body.slice(i, i + q.length)) + '' + 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 = "No articles yet. Use + Add to write the first one.";
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 => `
${esc(cat)} ${groups[cat].length}
${groups[cat].map(h => `
`).join('')}
`).join('') : `No article mentions "${esc(query)}".`;
}
// 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}Link`, copyPageLink);
mk('btn-print-doc', 'Print, or save as PDF with your browser', 'Print', 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 => ``).join('');
sel.style.display = 'none';
sel.onchange = () => {
const ed = document.getElementById('docEditor');
ed.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 ? '' : `${esc(currentTitle())}
`}${esc(date ? date.textContent : '')}${date ? ' · ' : ''}${esc(location.origin + '/docs/' + currentTitle())}
`;
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(`${esc(currentTitle())}
`);
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*(
)?\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 = `There's no article called "${esc(t)}". 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 = `\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');
});
});
})();