1
0
Fork 0
forked from jan/psslides

fix: bind :thisSlide per style block instead of per compile pass

Every slide's :thisSlide rules leaked into every other slide: the whole
joined source was recompiled once per slide with all occurrences rebound,
so each pass attached every slide's scoped rules to that pass's slide and
the concatenated output applied them all to everyone. Bind the token while
each block still belongs to one slide (global block -> any slide) and
compile the project once - also drops the O(n) duplicated CSS output.
This commit is contained in:
navi 2026-09-28 14:29:37 +02:00
parent 97ae327415
commit a05c860c6e

View file

@ -109,6 +109,9 @@ function slideSelector(id: string): string {
return `[data-ps-slide="${id}"]` 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' const THIS_SLIDE = ':thisSlide'
// `<?= … ?>` is a bogus comment to the HTML parser, so a placeholder would not // `<?= … ?>` is a bogus comment to the HTML parser, so a placeholder would not
@ -206,52 +209,40 @@ function isGlobalTemplate(global: string): boolean {
* Compile a project into runtime-ready data. * Compile a project into runtime-ready data.
* *
* Every `<style>` tag in the deck contributes to one project stylesheet (the * Every `<style>` tag in the deck contributes to one project stylesheet (the
* global slide first, then each slide in order). That stylesheet is compiled * global slide first, then each slide in order), and `:thisSlide` is rewritten
* once per slide, with `:thisSlide` replaced by the selector of the slide being * per style block *before* the blocks are joined: the global block to any
* compiled — so Less keeps behaving like Less, and the output never depends on * slide, each slide's block to that slide's own section. The joined source
* which slide happens to be visible. * then compiles exactly once — so Less keeps behaving like Less, the output
* never depends on which slide is visible, and one slide's scoped rules can
* never bind to its neighbours.
*/ */
export async function compilePresentation(input: CompileInput): Promise<CompiledPresentation> { export async function compilePresentation(input: CompileInput): Promise<CompiledPresentation> {
const global = extractStyles(input.global ?? '') const global = extractStyles(input.global ?? '')
const globalIsTemplate = isGlobalTemplate(global.html) const globalIsTemplate = isGlobalTemplate(global.html)
const sources: string[] = [global.style] const extracted = input.slides.map((markup) => extractStyles(markup))
const slides: CompiledSlide[] = input.slides.map((markup, index) => { const slides: CompiledSlide[] = extracted.map((slide, index) => ({
const extracted = extractStyles(markup)
sources.push(extracted.style)
return {
id: slideId(index), id: slideId(index),
index, index,
html: globalIsTemplate ? extracted.html : buildSlideHtml(extracted.html, global.html) html: globalIsTemplate ? slide.html : buildSlideHtml(slide.html, global.html)
} }))
})
const css = await compileStyles( // Bind `:thisSlide` while each block still belongs to exactly one slide:
sources.join('\n'), // the global block means "whoever is showing", a slide block means that
slides.map((slide) => slide.id) // slide. Rebinding after the join (the old per-slide passes) rebound every
// block on every pass, so each slide's scoped rules matched all slides.
const sources = [
bindThisSlide(global.style, ANY_SLIDE_SELECTOR),
...extracted.map((slide, index) =>
bindThisSlide(slide.style, slideSelector(slideId(index)))
) )
]
const source = sources.join('\n')
const css = source.trim() ? await renderLess(source) : ''
return { slides, css, template: global.html } return { slides, css, template: global.html }
} }
async function compileStyles(source: string, ids: string[]): Promise<string> { /** Rewrite the `:thisSlide` authoring token to a concrete selector. */
if (!source.trim()) { function bindThisSlide(source: string, selector: string): string {
return '' return source.split(THIS_SLIDE).join(selector)
}
// Fast path: without `:thisSlide` every slide resolves to the same CSS, so
// compile once instead of once per slide.
if (!source.includes(THIS_SLIDE)) {
return renderLess(source)
}
const perSlide = await Promise.all(
ids.map(async (id) => {
const selector = slideSelector(id)
const scoped = source.split(THIS_SLIDE).join(selector)
const css = await renderLess(scoped)
return `/* ${selector} */\n${css}`
})
)
return perSlide.join('\n')
} }