Skip to content

Commit

Permalink
Add light-dark() support (#26)
Browse files Browse the repository at this point in the history
* add light-dark support

* add light-dark() example to README

* fix

* fix

* review issues

* prevent from transfomring light-dark inside strings

* Update README.md

Co-authored-by: Andrey Sitnik <andrey@sitnik.ru>

* Update README.md

Co-authored-by: Andrey Sitnik <andrey@sitnik.ru>

* move link

---------

Co-authored-by: Andrey Sitnik <andrey@sitnik.ru>
  • Loading branch information
VladBrok and ai authored Feb 10, 2024
1 parent 68fa93b commit dcdb8b8
Show file tree
Hide file tree
Showing 3 changed files with 299 additions and 1 deletion.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ CSS solution for light/dark/auto theme switcher for websites.
by subset/sunrise (all operating systems now have theme switching schedule).

[PostCSS] plugin to make switcher to force dark or light theme by copying styles
from media query to special class.
from media query or [light-dark()] to special class.

[PostCSS]: https://github.com/postcss/postcss
[FART]: https://css-tricks.com/flash-of-inaccurate-color-theme-fart/
[light-dark()]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark

```css
/* Input CSS */
Expand All @@ -28,6 +29,10 @@ from media query to special class.
background: black
}
}

section {
background: light-dark(white, black);
}
```

```css
Expand All @@ -47,6 +52,23 @@ html:where(.is-dark) {
:where(html.is-dark) body {
background: black
}

@media (prefers-color-scheme: dark) {
:where(html:not(.is-light)) section {
background: black;
}
}
:where(html.is-dark) section {
background: black;
}
@media (prefers-color-scheme: light) {
:where(html:not(.is-dark)) section {
background: white;
}
}
:where(html.is-light) section {
background: white;
}
```

By default (without classes on `html`), website will use browser dark/light
Expand Down
58 changes: 58 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const PREFERS_COLOR_ONLY = /^\(\s*prefers-color-scheme\s*:\s*(dark|light)\s*\)$/
const PREFERS_COLOR = /\(\s*prefers-color-scheme\s*:\s*(dark|light)\s*\)/g
const LIGHT_DARK = /light-dark\(\s*(.+?)\s*,\s*(.+?)\s*\)/g
const STRING = /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/dg

function escapeRegExp(string) {
return string.replace(/[$()*+.?[\\\]^{|}-]/g, '\\$&')
Expand All @@ -9,6 +11,38 @@ function replaceAll(string, find, replace) {
return string.replace(new RegExp(escapeRegExp(find), 'g'), replace)
}

function addColorSchemeMedia(isDark, propValue, declaration, postcss) {
let mediaQuery = postcss.atRule({
name: 'media',
params: `(prefers-color-scheme:${isDark ? 'dark' : 'light'})`
})
mediaQuery.append(
postcss.rule({
nodes: [
postcss.decl({
prop: declaration.prop,
value: propValue
})
],
selector: declaration.parent.selector
})
)
declaration.parent.after(mediaQuery)
}

function replaceLightDark(isDark, declaration, stringBoundaries) {
return declaration.value.replaceAll(
LIGHT_DARK,
(match, lightColor, darkColor, offset) => {
let isInsideString = stringBoundaries.some(
boundary => offset > boundary[0] && offset < boundary[1]
)
if (isInsideString) return match
return isDark ? darkColor : lightColor
}
)
}

module.exports = (opts = {}) => {
let dark = opts.darkSelector || '.is-dark'
let light = opts.lightSelector || '.is-light'
Expand Down Expand Up @@ -109,6 +143,30 @@ module.exports = (opts = {}) => {
}
}
},
DeclarationExit: (declaration, { postcss }) => {
if (!declaration.value.includes('light-dark')) return

let stringBoundaries = []
let value = declaration.value.slice()
let match = STRING.exec(value)
while (match) {
stringBoundaries.push(match.indices[0])
match = STRING.exec(value)
}

let lightValue = replaceLightDark(false, declaration, stringBoundaries)
if (declaration.value === lightValue) return
let darkValue = replaceLightDark(true, declaration, stringBoundaries)

addColorSchemeMedia(false, lightValue, declaration, postcss)
addColorSchemeMedia(true, darkValue, declaration, postcss)

let parent = declaration.parent
declaration.remove()
if (parent.nodes.length === 0) {
parent.remove()
}
},
postcssPlugin: 'postcss-dark-theme-class'
}
}
Expand Down
218 changes: 218 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,4 +383,222 @@ test('ignores already transformed rules - light scheme', () => {
)
})

test('transforms light-dark()', () => {
run(
`html {
border: 1px solid light-dark(white, black)
}`,
`@media (prefers-color-scheme:dark) {
html:where(:not(.is-light)) {
border: 1px solid black
}
}
html:where(.is-dark) {
border: 1px solid black
}
@media (prefers-color-scheme:light) {
html:where(:not(.is-dark)) {
border: 1px solid white
}
}
html:where(.is-light) {
border: 1px solid white
}`
)
})

test('does not transform light-dark() inside strings', () => {
run(
`html {
content: ' light-dark(white, black) \
light-dark(purple, yellow)
';
background: url("light-dark(red, blue).png");
quotes: "light-dark(white, black)" "light-dark(red, green)";
}`,
`html {
content: ' light-dark(white, black) \
light-dark(purple, yellow)
';
background: url("light-dark(red, blue).png");
quotes: "light-dark(white, black)" "light-dark(red, green)";
}`
)
})

test('transforms light-dark() and disables :where() of request', () => {
run(
`section {
color: light-dark(#888, #eee)
}`,
`@media (prefers-color-scheme:dark) {
html:not(.is-light) section {
color: #eee
}
}
html.is-dark section {
color: #eee
}
@media (prefers-color-scheme:light) {
html:not(.is-dark) section {
color: #888
}
}
html.is-light section {
color: #888
}`,
{ useWhere: false }
)
})

test('processes inner at-rules with light-dark()', () => {
run(
`@media (min-width: 500px) {
@media (print) {
a {
background-color: light-dark(white, black)
}
}
}`,
`@media (min-width: 500px) {
@media (print) {
@media (prefers-color-scheme:dark) {
:where(html:not(.is-light)) a {
background-color: black
}
}
:where(html.is-dark) a {
background-color: black
}
@media (prefers-color-scheme:light) {
:where(html:not(.is-dark)) a {
background-color: white
}
}
:where(html.is-light) a {
background-color: white
}
}
}`
)
})

test('ignores whitespaces for light-dark()', () => {
run(
`a { background: radial-gradient(light-dark( red , yellow ),
light-dark( white , black ),
rgb(30 144 255)); }
`,
`@media (prefers-color-scheme:dark) {
:where(html:not(.is-light)) a {
background: radial-gradient(yellow,
black,
rgb(30 144 255))
}
}
:where(html.is-dark) a {
background: radial-gradient(yellow,
black,
rgb(30 144 255))
}
@media (prefers-color-scheme:light) {
:where(html:not(.is-dark)) a {
background: radial-gradient(red,
white,
rgb(30 144 255))
}
}
:where(html.is-light) a {
background: radial-gradient(red,
white,
rgb(30 144 255))
}
`
)
})

test('changes root selectors for light-dark()', () => {
run(
`html, .s { --bg: light-dark(white, black) }
p { color: light-dark(red, blue) }
`,
`@media (prefers-color-scheme:dark) {
html:where(:not(.is-light)), .s:where(:not(.is-light)) {
--bg: black
}
}
html:where(.is-dark), .s:where(.is-dark) {
--bg: black
}
@media (prefers-color-scheme:light) {
html:where(:not(.is-dark)), .s:where(:not(.is-dark)) {
--bg: white
}
}
html:where(.is-light), .s:where(.is-light) {
--bg: white
}
@media (prefers-color-scheme:dark) {
:where(html:not(.is-light)) p,:where(.s:not(.is-light)) p {
color: blue
}
}
:where(html.is-dark) p,:where(.s.is-dark) p {
color: blue
}
@media (prefers-color-scheme:light) {
:where(html:not(.is-dark)) p,:where(.s:not(.is-dark)) p {
color: red
}
}
:where(html.is-light) p,:where(.s.is-light) p {
color: red
}
`,
{ rootSelector: ['html', ':root', '.s'] }
)
})

test('changes root selector for light-dark()', () => {
run(
`body { --bg: light-dark(white, black) }
p { color: light-dark(green, yellow) }
`,
`@media (prefers-color-scheme:dark) {
body:where(:not(.is-light)) {
--bg: black
}
}
body:where(.is-dark) {
--bg: black
}
@media (prefers-color-scheme:light) {
body:where(:not(.is-dark)) {
--bg: white
}
}
body:where(.is-light) {
--bg: white
}
@media (prefers-color-scheme:dark) {
:where(body:not(.is-light)) p {
color: yellow
}
}
:where(body.is-dark) p {
color: yellow
}
@media (prefers-color-scheme:light) {
:where(body:not(.is-dark)) p {
color: green
}
}
:where(body.is-light) p {
color: green
}
`,
{ rootSelector: 'body' }
)
})

test.run()

0 comments on commit dcdb8b8

Please sign in to comment.