forked from jan/psslides
fix: scope every rule in a slide's own style block to that slide
Binding only the `:thisSlide` token still left every untokened rule in a slide's tag compiling deck-wide, so one slide's styles kept applying to all slides. Wrap each slide's whole style block in its section selector instead, with `:thisSlide` resolving to `&` — Less hoists `@keyframes`/ `@font-face` out of the wrapper itself and keeps `@media` nested inside, so only rules end up scoped. The global block stays the shared layer and the deck still compiles exactly once.
This commit is contained in:
parent
48e06bb689
commit
eeabb70855
2 changed files with 31 additions and 19 deletions
14
src/main.ts
14
src/main.ts
|
|
@ -14,15 +14,17 @@ interface Slide {
|
||||||
code: string
|
code: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// The starter slide opens with a <style> tag. Less written inside any <style>
|
// The starter slide opens with a <style> tag. Less written inside a slide's
|
||||||
// tag in the deck becomes the project stylesheet, and `:thisSlide` compiles to
|
// <style> tag is scoped to that slide, the global slide's styles are shared
|
||||||
// each slide's own selector, so deck styles stay scoped without inventing any
|
// by every slide, and `:thisSlide` is the slide's own section — no extra CSS
|
||||||
// extra CSS syntax.
|
// syntax beyond that.
|
||||||
const STARTER_STYLE = `// Project styles. This is ordinary Less: variables, nesting,
|
const STARTER_STYLE = `// Project styles. This is ordinary Less: variables, nesting,
|
||||||
// mixins and imports all work as usual.
|
// mixins and imports all work as usual.
|
||||||
//
|
//
|
||||||
// :thisSlide compiles to the selector of the slide this stylesheet is being
|
// A slide's own style tag is scoped to that slide: every rule in it applies
|
||||||
// compiled for, so it means "this slide", never "the visible slide".
|
// to that slide alone. The global slide's styles are shared by all slides.
|
||||||
|
// :thisSlide is the slide's own section, never "the visible slide" — handy
|
||||||
|
// for state rules: :thisSlide[data-ps-state='current'] { ... }.
|
||||||
|
|
||||||
@bg: #101010;
|
@bg: #101010;
|
||||||
@fg: #f0f0f0;
|
@fg: #f0f0f0;
|
||||||
|
|
|
||||||
|
|
@ -209,12 +209,13 @@ 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), and `:thisSlide` is rewritten
|
* global slide first, then each slide in order). The global block is the
|
||||||
* per style block *before* the blocks are joined: the global block to any
|
* shared layer and compiles deck-wide; each slide's block is *wrapped* in its
|
||||||
* slide, each slide's block to that slide's own section. The joined source
|
* section selector before the blocks are joined, so a slide's stylesheet
|
||||||
* then compiles exactly once — so Less keeps behaving like Less, the output
|
* styles that slide and nothing else — token or not — with `:thisSlide`
|
||||||
* never depends on which slide is visible, and one slide's scoped rules can
|
* resolving to the section itself. The joined source compiles exactly once:
|
||||||
* never bind to its neighbours.
|
* Less stays Less, the output never depends on which slide is visible, and
|
||||||
|
* one slide's rules can never reach 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 ?? '')
|
||||||
|
|
@ -227,15 +228,13 @@ export async function compilePresentation(input: CompileInput): Promise<Compiled
|
||||||
html: globalIsTemplate ? slide.html : buildSlideHtml(slide.html, global.html)
|
html: globalIsTemplate ? slide.html : buildSlideHtml(slide.html, global.html)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Bind `:thisSlide` while each block still belongs to exactly one slide:
|
// Scope each block while it still belongs to exactly one slide: the
|
||||||
// the global block means "whoever is showing", a slide block means that
|
// global block means "everywhere", a slide block means that slide. Only
|
||||||
// slide. Rebinding after the join (the old per-slide passes) rebound every
|
// rewriting the `:thisSlide` token (the previous fix) still left every
|
||||||
// block on every pass, so each slide's scoped rules matched all slides.
|
// untokened rule deck-wide — the whole block now carries the selector.
|
||||||
const sources = [
|
const sources = [
|
||||||
bindThisSlide(global.style, ANY_SLIDE_SELECTOR),
|
bindThisSlide(global.style, ANY_SLIDE_SELECTOR),
|
||||||
...extracted.map((slide, index) =>
|
...extracted.map((slide, index) => scopeBlock(slide.style, slideSelector(slideId(index))))
|
||||||
bindThisSlide(slide.style, slideSelector(slideId(index)))
|
|
||||||
)
|
|
||||||
]
|
]
|
||||||
const source = sources.join('\n')
|
const source = sources.join('\n')
|
||||||
const css = source.trim() ? await renderLess(source) : ''
|
const css = source.trim() ? await renderLess(source) : ''
|
||||||
|
|
@ -246,3 +245,14 @@ export async function compilePresentation(input: CompileInput): Promise<Compiled
|
||||||
function bindThisSlide(source: string, selector: string): string {
|
function bindThisSlide(source: string, selector: string): string {
|
||||||
return source.split(THIS_SLIDE).join(selector)
|
return source.split(THIS_SLIDE).join(selector)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scope a slide's whole style block to its section: wrap the block in the
|
||||||
|
* slide's selector and let `:thisSlide` become `&` — the section itself.
|
||||||
|
* Less hoists `@keyframes`/`@font-face` out of the wrapper on its own and
|
||||||
|
* keeps `@media` nested inside it, so only rules end up scoped.
|
||||||
|
*/
|
||||||
|
function scopeBlock(source: string, selector: string): string {
|
||||||
|
if (!source.trim()) return ''
|
||||||
|
return `${selector} {\n${source.split(THIS_SLIDE).join('&')}\n}`
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue