Redirecting to the presentation...
+ + +`; +} + +export async function collectDeckFiles(rootDir) { + const decksRoot = path.join(rootDir, 'Decks'); + const results = []; + + async function walk(relDir) { + const absDir = path.join(decksRoot, relDir); + const entries = await fs.readdir(absDir, { withFileTypes: true }); + for (const entry of entries) { + const rel = relDir ? path.posix.join(relDir, entry.name) : entry.name; + if (entry.isDirectory()) { + await walk(rel); + continue; + } + if ( + entry.isFile() && + rel.toLowerCase().endsWith('.html') && + path.posix.basename(rel) !== 'index.html' + ) { + results.push({ + publicPath: rel, + absolutePath: path.join(decksRoot, rel), + }); + } + } + } + + await walk(''); + results.sort((a, b) => a.publicPath.localeCompare(b.publicPath)); + return results; +} diff --git a/scripts/site-indexes.mjs b/scripts/site-indexes.mjs index a3eb83d..7475bbb 100644 --- a/scripts/site-indexes.mjs +++ b/scripts/site-indexes.mjs @@ -2,6 +2,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { syncDeckHosting } from '../config/site-map.mjs'; +import { extractPermalinkHash } from './permalink-utils.mjs'; const NATURAL_COLLATOR = new Intl.Collator(undefined, { numeric: true, @@ -132,10 +133,10 @@ export const PAGE_STYLE = ` .file-actions a { color: inherit; } .file-actions a:hover { color: var(--accent); text-decoration: underline; } .file-actions .sep { color: #3d537e; } - .file-actions button.copy-url { + .tree button.copy-url { display: inline-flex; align-items: center; - color: inherit; + color: var(--muted); background: none; border: none; padding: 2px; @@ -144,12 +145,13 @@ export const PAGE_STYLE = ` border-radius: 4px; cursor: pointer; } - .file-actions button.copy-url svg { width: 14px; height: 14px; display: block; } - .file-actions button.copy-url:hover { color: var(--accent); } - .file-actions button.copy-url:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; } - .file-actions button.copy-url.copied { color: #7cd992; } - .file-actions button.copy-url.failed { color: #ff8a80; } - .file-actions button.copy-url:disabled { cursor: default; } + .tree button.copy-url svg { width: 14px; height: 14px; display: block; } + .tree button.copy-url:hover { color: var(--accent); } + .tree button.copy-url:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; } + .tree button.copy-url.copied { color: #7cd992; } + .tree button.copy-url.failed { color: #ff8a80; } + .tree button.copy-url:disabled { cursor: default; } + .tree button.copy-url-labeled { gap: 4px; } @media (max-width: 640px) { .tree li.file { flex-direction: column; @@ -201,9 +203,10 @@ export const PAGE_SCRIPT = ` document.querySelectorAll('a[data-launch-path]').forEach((a) => { const presentationUrl = resolveUrl(a.getAttribute('data-launch-path') || ''); - if (a.getAttribute('data-launch') === 'instructor') { + const launchMode = a.getAttribute('data-launch'); + if (launchMode === 'instructor') { a.href = buildActiveBitsUrl(launchPath, presentationUrl, { mode: 'instructor' }); - } else { + } else if (launchMode === 'syncdeck') { a.href = buildActiveBitsUrl(permalinkPath, presentationUrl); } }); @@ -277,6 +280,15 @@ export async function defaultTitleForFile(filePath) { return path.basename(filePath, path.extname(filePath)).replace(/[-_]/g, ' ').trim(); } +export async function permalinkHashForFile(filePath) { + try { + const text = await fs.readFile(filePath, 'utf8'); + return extractPermalinkHash(text); + } catch { + return null; + } +} + function makePage(titleText, heading, description, listing, generatedAt, generatedAtIso, backLink = null) { const backHtml = backLink ? `` @@ -307,19 +319,31 @@ ${listing} `; } -function renderTree(node, indent = ' ', pathPrefix = '') { +function renderTree(node, indent = ' ', pathPrefix = '', pageDepth = 0) { const lines = []; + // pageDepth is how many directories the *page being rendered* sits below + // the site root (constant across this recursion), not how deep `pathPrefix` + // has nested within that single page's listing — those are different axes. + const rootPrefix = '../'.repeat(pageDepth); for (const file of [...node.files].sort((a, b) => NATURAL_COLLATOR.compare(a.title, b.title))) { const relativeHref = `${pathPrefix}${file.name}`; + const permalinkHref = file.permalinkHash ? `${rootPrefix}p/${file.permalinkHash}.html` : null; + // The syncdeck link is fed to ActiveBits as presentationUrl, so route it + // through our own stable permalink when one exists rather than the raw + // (reorg-fragile) path. + const syncdeckLaunchPath = permalinkHref || relativeHref; lines.push( `${indent}