From a7e463cb94236a1a14f5192502453ecc428ea26f Mon Sep 17 00:00:00 2001 From: navi Date: Sun, 27 Sep 2026 22:36:46 +0200 Subject: [PATCH] feat: render the global slide as a / template --- src/main.ts | 13 +++-- src/slides/compile.ts | 58 ++++++++++++++++++--- src/slides/placeholders.ts | 13 +++++ src/slides/runtime.ts | 100 ++++++++++++++++++++++++++++++++++++- 4 files changed, 168 insertions(+), 16 deletions(-) create mode 100644 src/slides/placeholders.ts diff --git a/src/main.ts b/src/main.ts index 86307f4..4345a2a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -72,12 +72,9 @@ const slideElements = new Map() const thumbnailRequests = new Map() let thumbnailDebounceTimer: number | undefined -// Project CSS lives in its own stylesheet so the preview can be swapped out -// without touching the editor's own styles. -const slideStyles = document.createElement('style') -slideStyles.setAttribute('data-ps-slides', '') -document.head.append(slideStyles) - +// The compiled stylesheet is the runtime's business: it lands at the global +// slide's ``, or in a `data-ps-slides` sheet in the head when the +// scene has no injection point. let runtime: Runtime | null = null let compileDebounceTimer: number | undefined // Set while we push text into Monaco ourselves, so programmatic changes are @@ -141,11 +138,11 @@ async function rebuildPreview(): Promise { global: globalSlide }) - slideStyles.textContent = result.css compileError.hidden = true compileError.textContent = '' if (runtime) { + runtime.setTemplate(result.template) runtime.setSlides(result.slides) // Follow the slide being edited; the deck may just have grown. if (currentSlide >= 0) { @@ -153,9 +150,11 @@ async function rebuildPreview(): Promise { } } else { runtime = createRuntime(stage, result.slides, { + template: result.template, onCurrentChange: (index) => setSlide(index) }) } + runtime.setCss(result.css) } catch (error) { // Half-typed Less should not blank the preview: keep the last good // output on screen and report what broke. diff --git a/src/slides/compile.ts b/src/slides/compile.ts index 3dcd312..4b3c72e 100644 --- a/src/slides/compile.ts +++ b/src/slides/compile.ts @@ -1,4 +1,5 @@ import less from 'less' +import { CSS_PLACEHOLDER, HTML_PLACEHOLDER } from './placeholders' /** * A slide's stable id. Deterministic for a given position, so recompiling the @@ -14,6 +15,12 @@ function slideSelector(id: string): string { 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 @@ -24,13 +31,22 @@ export interface CompiledSlide { export interface CompileInput { /** HTML source of every slide, in presentation order. */ slides: string[] - /** HTML shared by every slide (the "global" slide in the editor). */ + /** + * 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 } /** @@ -39,26 +55,53 @@ export interface CompiledPresentation { * The markup stays HTML and the tag contents become the project's Less, so * authors write styles right in the slide they are editing. The tags are * removed from the markup because their raw Less must never reach the DOM — - * the compiled CSS is injected separately. + * the compiled CSS is injected separately, at ``. A style tag that + * *is* that injection point is left alone. */ export function extractStyles(markup: string): { html: string; style: string } { if (!markup.includes('