import { parseHTML } from "linkedom"; import { composeSlidesHtml } from "./presentation.js"; export interface DeckSlide { /** 1-based deck position — the number the runtime counter shows. */ index: number; /** Source file, `slides/`-relative. */ file: string; /** 1-based ordinal of this slide within its source file. */ slideInFile: number; /** How many slides the source file contains. */ slidesInFile: number; /** Text of the slide's first heading, if any. */ title: string; } interface SlideElement { getAttribute(name: string): string | null; querySelector(selector: string): { textContent: string | null } | null; } export async function listDeckSlides(rootDir: string): Promise { const slidesHtml = await composeSlidesHtml(rootDir); const { document } = parseHTML(`${slidesHtml}`) as unknown as { document: { querySelectorAll(selector: string): { length: number; [index: number]: unknown } }; }; const nodes = document.querySelectorAll(".slide"); const out: DeckSlide[] = []; for (let i = 0; i < nodes.length; i++) { const el = nodes[i] as SlideElement; const [inFile = "1", ofFile = "1"] = (el.getAttribute("data-zerp-src-slide") ?? "1/1").split( "/", ); out.push({ index: Number.parseInt(el.getAttribute("data-zerp-index") ?? "", 10) || i + 1, file: el.getAttribute("data-zerp-src") ?? "", slideInFile: Number.parseInt(inFile, 10) || 1, slidesInFile: Number.parseInt(ofFile, 10) || 1, title: (el.querySelector("h1, h2, h3")?.textContent ?? "").replace(/\s+/g, " ").trim(), }); } return out; } export function formatSlideList(slides: DeckSlide[]): string { const indexWidth = Math.max(1, String(slides.length).length); const fileWidth = Math.max(4, ...slides.map((slide) => slide.file.length)); const lines = [`${"#".padStart(indexWidth)} ${"file".padEnd(fileWidth)} in-file title`]; for (const slide of slides) { lines.push( [ String(slide.index).padStart(indexWidth), slide.file.padEnd(fileWidth), `${slide.slideInFile}/${slide.slidesInFile}`.padEnd(7), slide.title, ] .join(" ") .trimEnd(), ); } return `${lines.join("\n")}\n`; }