Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Match converted :root specificity to original selector exactly #333

Merged
merged 6 commits into from
May 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Upgrade dependent packages to the latest version ([#332](https://github.com/marp-team/marpit/pull/332))

### Fixed

- Match `:root` selector specificity to original exactly ([#330](https://github.com/marp-team/marpit/issues/330), [#333](https://github.com/marp-team/marpit/pull/333))

### Removed

- Continuous test against Node.js 10 ([#291](https://github.com/marp-team/marpit/issues/291), [#332](https://github.com/marp-team/marpit/pull/332))
Expand Down
24 changes: 13 additions & 11 deletions docs/theme-css.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,9 @@ h2 {

We have no any extra classes or mixins, and do almost not need require to know extra rules for creating theme. This is a key factor of Marpit different from other slide framework.

### Metadata

The `@theme` metadata is always required by Marpit. Define metadata through CSS comment.

```css
/* @theme name */
```

!> You should use the `/*! comment */` syntax to prevent removing comments if you're using the compressed output of [Sass].

### `:root` pseudo-class selector

Since v1.6.0, [`:root` pseudo-class](https://developer.mozilla.org/en-US/docs/Web/CSS/:root) indicates the viewport of each slide pages in the context of Marpit theme CSS, by replacing `:root` into `section` automatically.
In the context of Marpit, [`:root` pseudo-class](https://developer.mozilla.org/en-US/docs/Web/CSS/:root) indicates each `<section>` elements for the slide page instead of `<html>`.

The following is similar theme definition to the example shown earlier, but it's using `:root` selector.

Expand All @@ -75,6 +65,18 @@ h2 {

[`rem` units](https://developer.mozilla.org/en-US/docs/Web/CSS/font-size#Rems) in Marpit theme will automatically transform into the calculated relative value from the parent `<section>` element, so anyone don't have to worry the effect from `font-size` in the root `<html>` that placed Marpit slide. Everything would work as the theme author expected.

?> `:root` selector can use just like as `section` selector, but there is a difference that `:root` has higher [CSS specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity) than `section`. If both selectors have mixed in a theme CSS, declarations in `:root` selector will be prefered than `section` selector.

### Metadata

**The `@theme` metadata is always required by Marpit.** You must define metadata through CSS comment.

```css
/* @theme name */
```

!> You should use the `/*! comment */` syntax to prevent removing comments if you're using the compressed output of [Sass].

## Styling

### Slide size
Expand Down
11 changes: 6 additions & 5 deletions src/postcss/root/increasing_specificity.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import postcssPlugin from '../../helpers/postcss_plugin'

export const pseudoClass = ':marpit-root'

const matcher = new RegExp(`\\b${pseudoClass}\\b`, 'g')
const matcher = new RegExp(`\\b(?:section)?${pseudoClass}\\b`, 'g')

/**
* Marpit PostCSS root increasing specificity plugin.
*
* Replace `:marpit-root` pseudo-class selector into `:not(a)`, to increase
* specificity. `:marpit-root` is always added to `section` selector by root
* replace plugin so `:not(a)` must always match too.
* Replace specific pseudo-class selector to `:where(section):not([\20 root])`,
* to increase specificity. `:marpit-root` is always added to `section` selector
* by root replace plugin so `:where(section):not([\20 root])` must always match
* too (HTML does not allow U+0020 SPACE in the attribute name.).
*
* @alias module:postcss/root/increasing_specificity
*/
Expand All @@ -19,7 +20,7 @@ const plugin = postcssPlugin(
() => (css) =>
css.walkRules((rule) => {
rule.selectors = rule.selectors.map((selector) =>
selector.replace(matcher, ':not(a)')
selector.replace(matcher, ':where(section):not([\\20 root])')
)
})
)
Expand Down
4 changes: 3 additions & 1 deletion src/postcss/root/replace.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import postcssPlugin from '../../helpers/postcss_plugin'
/**
* Marpit PostCSS root replace plugin.
*
* Replace `:root` pseudo-class selector into `section`.
* Replace `:root` pseudo-class selector into `section`. It can add custom
* pseudo class through `pseudoClass` option to make distinguishable from
* `section` selector.
*
* @alias module:postcss/root/replace
*/
Expand Down
34 changes: 29 additions & 5 deletions src/postcss/section_size.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,48 @@ import postcssPlugin from '../helpers/postcss_plugin'
*/
const plugin = postcssPlugin(
'marpit-postcss-section-size',
({ pseudoClass } = {}) => {
({ preferedPseudoClass } = {}) => {
const rootSectionMatcher = new RegExp(
`^(?:section|\\*?:root)${pseudoClass ? `(?:${pseudoClass})?` : ''}$`
`^section${preferedPseudoClass ? `(${preferedPseudoClass})?` : ''}$`
)

return (css, { result }) => {
result.marpitSectionSize = result.marpitSectionSize || {}
const originalSize = result.marpitSectionSize || {}
const detectedSize = {}
const preferedSize = {}

let matched

css.walkRules((rule) => {
if (rule.selectors.some((s) => rootSectionMatcher.test(s))) {
if (
rule.selectors.some((s) => {
matched = s.match(rootSectionMatcher)
return !!matched
})
) {
rule.walkDecls(/^(width|height)$/, (decl) => {
const { prop } = decl
const value = decl.value.trim()

result.marpitSectionSize[prop] = value
if (matched[1]) {
preferedSize[prop] = value
} else {
detectedSize[prop] = value
}
})
}
})

const width =
preferedSize.width || detectedSize.width || originalSize.width

const height =
preferedSize.height || detectedSize.height || originalSize.height

result.marpitSectionSize = { ...originalSize }

if (width) result.marpitSectionSize.width = width
if (height) result.marpitSectionSize.height = height
}
}
)
Expand Down
5 changes: 4 additions & 1 deletion src/theme.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import postcss from 'postcss'
import postcssImportParse from './postcss/import/parse'
import postcssMeta from './postcss/meta'
import { pseudoClass } from './postcss/root/increasing_specificity'
import postcssRootReplace from './postcss/root/replace'
import postcssSectionSize from './postcss/section_size'
import skipThemeValidationSymbol from './theme/symbol'

Expand Down Expand Up @@ -99,7 +101,8 @@ class Theme {

const { css, result } = postcss([
postcssMeta({ metaType }),
postcssSectionSize,
postcssRootReplace({ pseudoClass }),
postcssSectionSize({ preferedPseudoClass: pseudoClass }),
postcssImportParse,
]).process(cssString)

Expand Down
16 changes: 11 additions & 5 deletions test/postcss/root/increasing_specificity.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ describe('Marpit PostCSS root increasing specificity plugin', () => {
from: undefined,
})

it('replaces specific pseudo-class into ":not(a)" to increase specificity', () => {
expect(run(`section${pseudoClass} {}`).css).toBe('section:not(a) {}')
it('replaces specific pseudo-class into ":where(section):not([\\20 root])" to increase specificity', () => {
expect(run(`section${pseudoClass} {}`).css).toBe(
':where(section):not([\\20 root]) {}'
)

// With replaced :root selector via root replace plugin
expect(run(`:root {}`).css).toBe('section:not(a) {}')
expect(run(`section :root {}`).css).toBe('section section:not(a) {}')
expect(run(`:root.klass div {}`).css).toBe('section:not(a).klass div {}')
expect(run(`:root {}`).css).toBe(':where(section):not([\\20 root]) {}')
expect(run(`section :root {}`).css).toBe(
'section :where(section):not([\\20 root]) {}'
)
expect(run(`:root.klass div {}`).css).toBe(
':where(section):not([\\20 root]).klass div {}'
)
})
})
17 changes: 17 additions & 0 deletions test/postcss/section_size.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,21 @@ describe('Marpit PostCSS section size plugin', () => {
run('section:first-child { width: 123px; height: 456px; }').then((result) =>
expect(result.marpitSectionSize).toStrictEqual({})
))

context('with preferedPseudoClass', () => {
const run = (input) =>
postcss([sectionSize({ preferedPseudoClass: ':test' })]).process(input, {
from: undefined,
})

it('prefers defined size within section selector with specific pseudo selector than plain selector', () =>
run(
'section:test { width: 123px; height: 123px; } section { width: 456px; height: 456px; } '
).then((result) =>
expect(result.marpitSectionSize).toStrictEqual({
width: '123px',
height: '123px',
})
))
})
})
6 changes: 5 additions & 1 deletion test/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,13 @@ describe('Theme', () => {
width: 960px;
height: 720px;
}
section {
width: 123px;
height: 456px;
}
`)

it('returns Theme instance that has width and height props', () => {
it('returns Theme instance that has width and height props defined in :root selector', () => {
expect(instance.width).toBe('960px')
expect(instance.height).toBe('720px')
})
Expand Down