import LessWorker from './less-worker?worker' import { CSS_PLACEHOLDER, HTML_PLACEHOLDER } from './placeholders' /** * Less runs in a worker so a heavy stylesheet never blocks the editor. The * worker is an optimisation, not a requirement: if it fails to start or dies * mid-render, everything still in flight (and every later render) falls back * to an on-demand `import('less')` on the main thread — which also keeps the * big Less bundle out of the initial page load. */ interface RenderTask { source: string resolve: (css: string) => void reject: (error: Error) => void } /** Reply the worker posts back: either the CSS or the failure message. */ interface RenderReply { id: number css?: string error?: string } const renderTasks = new Map() let nextRenderTask = 0 /** `undefined` until first use, then the worker — or `null` once degraded. */ let lessWorker: Worker | null | undefined function renderInline(source: string): Promise { // The same auto-init as in the worker: pre-set `onReady: false` (the init // copies `window.less` before its own defaults) so importing less does // not hide `` or rescan the app's stylesheets just because we only // wanted its compiler. if (typeof window !== 'undefined' && !('less' in window)) { ;(window as unknown as { less?: unknown }).less = { onReady: false } } return import('less').then(async (module) => { const { css } = await module.default.render(source) return css }) } /** The worker is gone: re-run everything pending on the main thread. */ function degradeWorker(): void { if (!lessWorker) { return } lessWorker.terminate() lessWorker = null const pending = Array.from(renderTasks.values()) renderTasks.clear() for (const task of pending) { renderInline(task.source).then(task.resolve).catch(task.reject) } } function getLessWorker(): Worker | null { if (lessWorker !== undefined) { return lessWorker } try { const worker = new LessWorker() worker.addEventListener('message', (event: MessageEvent) => { const task = renderTasks.get(event.data.id) if (!task) { return } renderTasks.delete(event.data.id) if (event.data.error !== undefined) { task.reject(new Error(event.data.error)) } else { task.resolve(event.data.css ?? '') } }) // Uncaught worker failure (bundle load, environment): drop to inline. worker.addEventListener('error', (event) => { event.preventDefault() degradeWorker() }) lessWorker = worker } catch { lessWorker = null } return lessWorker } function renderLess(source: string): Promise { const worker = getLessWorker() if (!worker) { return renderInline(source) } return new Promise((resolve, reject) => { const id = ++nextRenderTask renderTasks.set(id, { source, resolve, reject }) // oxlint-disable-next-line unicorn/require-post-message-target-origin worker.postMessage({ id, source }) }) } /** * A slide's stable id. Deterministic for a given position, so recompiling the * project (on every keystroke) never renames a slide underneath the runtime. */ export function slideId(index: number): string { return `slide-${index}` } function slideSelector(id: string): string { return `[data-ps-slide="${id}"]` } /** What `:thisSlide` means inside the global slide's styles: any slide. */ const ANY_SLIDE_SELECTOR = '[data-ps-slide]' const THIS_SLIDE = ':thisSlide' // `` is a bogus comment to the HTML parser, so a placeholder would not // survive the DOM round-trip inside extractStyles. Hide the placeholders as // plain text while parsing, put them back afterwards. const HTML_SENTINEL = '__PSHTML__' const CSS_SENTINEL = '__PSCSS__' export interface CompiledSlide { /** Stable id, used as the value of the slide's `data-ps-slide` attribute. */ id: string index: number html: string } export interface CompileInput { /** HTML source of every slide, in presentation order. */ slides: string[] /** * The global slide: either markup shared by every slide, or a template * carrying `` / `` for the runtime to fill in. */ global?: string } export interface CompiledPresentation { slides: CompiledSlide[] css: string /** * The global slide, ready for the runtime. A template (`` * inside) is rendered once as the scene; anything else was already merged * into every slide and the runtime ignores it. */ template: string } /** * Pull `