diff options
| -rw-r--r-- | src/components/MDX/CodeBlock/CodeBlock.tsx | 1 | ||||
| -rw-r--r-- | src/pages/article/[slug].tsx | 1 | ||||
| -rw-r--r-- | src/styles/base/_colors.scss | 40 | ||||
| -rw-r--r-- | src/styles/vendors/_prism.scss | 10 | ||||
| -rw-r--r-- | src/utils/plugins/prism-color-scheme.js | 252 | ||||
| -rw-r--r-- | tsconfig.json | 7 |
6 files changed, 309 insertions, 2 deletions
diff --git a/src/components/MDX/CodeBlock/CodeBlock.tsx b/src/components/MDX/CodeBlock/CodeBlock.tsx index 8311999..a822744 100644 --- a/src/components/MDX/CodeBlock/CodeBlock.tsx +++ b/src/components/MDX/CodeBlock/CodeBlock.tsx @@ -4,6 +4,7 @@ import { useRouter } from 'next/router'; import Prism from 'prismjs'; import { ReactChildren, useEffect } from 'react'; import { useIntl } from 'react-intl'; +import '@utils/plugins/prism-color-scheme'; const CodeBlock = ({ className, diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx index dc2c76a..1799fb0 100644 --- a/src/pages/article/[slug].tsx +++ b/src/pages/article/[slug].tsx @@ -20,6 +20,7 @@ import { ParsedUrlQuery } from 'querystring'; import { useEffect } from 'react'; import { useIntl } from 'react-intl'; import { Blog, BlogPosting, Graph, WebPage } from 'schema-dts'; +import '@utils/plugins/prism-color-scheme'; const SingleArticle: NextPageWithLayout<ArticleProps> = ({ post }) => { const { diff --git a/src/styles/base/_colors.scss b/src/styles/base/_colors.scss index 5d945bb..a927368 100644 --- a/src/styles/base/_colors.scss +++ b/src/styles/base/_colors.scss @@ -66,3 +66,43 @@ --color-token-yellow: #{var.$dark-theme_yellow}; --color-token-orange: #{var.$dark-theme_orange}; } + +pre[data-prismjs-color-scheme="light"] { + --color-bg: #{var.$light-theme_white}; + --color-bg-secondary: #{var.$light-theme_white-dark}; + --color-bg-tertiary: #{var.$light-theme_grey-bright}; + --color-fg: #{var.$light-theme_black}; + --color-fg-light: #{var.$light-theme_grey-dark}; + --color-primary: #{var.$light-theme_blue}; + --color-primary-darker: #{var.$light-theme_blue-darker}; + --color-border: #{var.$light-theme_grey}; + --color-border-dark: #{var.$light-theme_grey-dark}; + --color-token-red: #{var.$light-theme_red}; + --color-token-green: #{var.$light-theme_green}; + --color-token-purple: #{var.$light-theme_purple}; + --color-token-magenta: #{var.$light-theme_magenta}; + --color-token-cyan: #{var.$light-theme_cyan}; + --color-token-blue: #{var.$light-theme_blue}; + --color-token-yellow: #{var.$light-theme_yellow}; + --color-token-orange: #{var.$light-theme_orange}; +} + +pre[data-prismjs-color-scheme="dark"] { + --color-bg: #{var.$dark-theme_black}; + --color-bg-secondary: #{var.$dark-theme_black-bright}; + --color-bg-tertiary: #{var.$dark-theme_grey-darker}; + --color-fg: #{var.$dark-theme_white}; + --color-fg-light: #{var.$dark-theme_grey}; + --color-primary: #{var.$dark-theme_blue}; + --color-primary-darker: #{var.$dark-theme_blue-darker}; + --color-border: #{var.$dark-theme_grey-dark}; + --color-border-dark: #{var.$dark-theme_grey}; + --color-token-red: #{var.$dark-theme_red}; + --color-token-green: #{var.$dark-theme_green}; + --color-token-purple: #{var.$dark-theme_purple}; + --color-token-magenta: #{var.$dark-theme_magenta}; + --color-token-cyan: #{var.$dark-theme_cyan}; + --color-token-blue: #{var.$dark-theme_blue}; + --color-token-yellow: #{var.$dark-theme_yellow}; + --color-token-orange: #{var.$dark-theme_orange}; +} diff --git a/src/styles/vendors/_prism.scss b/src/styles/vendors/_prism.scss index 8328114..87eba2c 100644 --- a/src/styles/vendors/_prism.scss +++ b/src/styles/vendors/_prism.scss @@ -49,6 +49,11 @@ } } } + + .toolbar-item:nth-child(3) { + order: 3; + margin-left: var(--spacing-2xs); + } } pre[class*="language-"] { @@ -56,6 +61,8 @@ pre[class*="language-"] { margin: var(--spacing-md) 0; padding: 0; position: relative; + background: var(--color-bg-secondary); + color: var(--color-fg); border: fun.convert-px(1) solid var(--color-border); > code { @@ -235,7 +242,8 @@ pre.command-line { } } -.copy-to-clipboard-button { +.copy-to-clipboard-button, +.prism-color-scheme-button { display: block; padding: 0 var(--spacing-xs); background: var(--color-bg); diff --git a/src/utils/plugins/prism-color-scheme.js b/src/utils/plugins/prism-color-scheme.js new file mode 100644 index 0000000..93a8e7a --- /dev/null +++ b/src/utils/plugins/prism-color-scheme.js @@ -0,0 +1,252 @@ +(function () { + if (typeof Prism === 'undefined' || typeof document === 'undefined') { + return; + } + + if (!Prism.plugins.toolbar) { + console.warn('Color scheme plugin loaded before Toolbar plugin.'); + + return; + } + + /** + * + * @typedef {"dark" | "light" | "system"} Theme + * @typedef {Record<"color-scheme", Theme> & Record<"button-prefix" | "dark" | "light", string>} Settings + */ + + var storage = { + /** + * Get a deserialized value from local storage. + * + * @param {string} key - The local storage key. + * @returns {string | undefined} The value of the given key. + */ + get: function (key) { + var serializedItem = localStorage.getItem(key); + return serializedItem ? JSON.parse(serializedItem) : undefined; + }, + /** + * Set or update a local storage key with a new serialized value. + * + * @param {string} key - The local storage key. + * @param {string} value - The value of the given key. + */ + set: function (key, value) { + var serializedValue = JSON.stringify(value); + localStorage.setItem(key, serializedValue); + }, + }; + + /** + * Check if user has set its color scheme preference. + * + * @returns {boolean} True if user prefers dark color scheme. + */ + function prefersDarkScheme() { + return ( + window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches + ); + } + + /** + * Get the theme that matches the system theme. + * + * @returns {Theme} The theme to use. + */ + function getThemeFromSystem() { + return prefersDarkScheme() ? 'dark' : 'light'; + } + + /** + * Check if the provided string is a valid theme. + * + * @param {string} theme - A theme to check. + * @returns {boolean} True if it is a valid theme. + */ + function isValidTheme(theme) { + return theme === 'dark' || theme === 'light' || theme === 'system'; + } + + /** + * Set the default theme depending on user preferences. + * + * @returns {Theme} The default theme. + */ + function setDefaultTheme() { + var theme = storage.get('prismjs-color-scheme'); + + return theme && isValidTheme(theme) ? theme : 'system'; + } + + /** + * Traverses up the DOM tree to find data attributes that override the + * default plugin settings. + * + * @param {Element} startElement - An element to start from. + * @returns {Settings} The plugin settings. + */ + function getSettings(startElement) { + /** @type Settings */ + var settings = { + 'color-scheme': setDefaultTheme(), + dark: 'Dark Theme', + light: 'Light Theme', + 'button-prefix': 'Toggle', + }; + var prefix = 'data-prismjs-'; + + for (var key in settings) { + var attr = prefix + key; + var element = startElement; + + while (element && !element.hasAttribute(attr)) { + element = element.parentElement; + } + + if (element) { + settings[key] = element.getAttribute(attr); + } + } + + return settings; + } + + /** + * Retrieve the new theme depending on current theme value. + * + * @param {Theme} currentTheme - The current theme. + * @returns {Theme} The new theme. + */ + function getNewTheme(currentTheme) { + switch (currentTheme) { + case 'light': + return 'dark'; + case 'dark': + return 'light'; + case 'system': + default: + return getNewTheme(getThemeFromSystem()); + } + } + + /** + * Get the button content depending on current theme. + * + * @param {string} prefix - The text prefix. + * @param {Theme} theme - The current theme. + * @param {Settings} settings - The plugin settings. + * @returns {string} The button text. + */ + function getButtonContent(prefix, theme, settings) { + if (theme === 'dark') { + return `${prefix}${settings['light']}`; + } + + return `${prefix}${settings['dark']}`; + } + + /** + * Update the button text depending on the current theme. + * + * @param {HTMLButtonElement} button - The color scheme button. + * @param {Settings} settings - The plugin settings. + */ + function updateButtonText(button, settings) { + var prefix = settings['button-prefix'] + ? `${settings['button-prefix']} ` + : ''; + var theme = settings['color-scheme']; + + if (theme === 'system') { + theme = getThemeFromSystem(); + } + + button.textContent = getButtonContent(prefix, theme, settings); + } + + /** + * Update pre data-prismjs-color-scheme attribute. + * + * @param {HTMLPreElement} pre - The pre element wrapping the code. + * @param {Theme} theme - The current theme. + */ + function updatePreAttribute(pre, theme) { + pre.setAttribute('data-prismjs-color-scheme', theme); + } + + /** + * Update pre attribute for all code blocks. + * + * @param {Theme} theme - The new theme. + */ + function switchTheme(theme) { + var allPre = document.querySelectorAll('pre[data-prismjs-color-scheme]'); + allPre.forEach((pre) => { + updatePreAttribute(pre, theme); + }); + } + + /** + * Set current theme on pre attribute change. + * + * @param {HTMLPreElement} pre - The pre element wrapping the code. + * @param {Settings} settings - The plugin settings. + */ + function listenAttributeChange(pre, settings) { + var observer = new MutationObserver(function (mutations) { + mutations.forEach((record) => { + var mutatedPre = record.target; + var button = mutatedPre.parentElement.querySelector( + '.prism-color-scheme-button' + ); + var newTheme = mutatedPre.getAttribute('data-prismjs-color-scheme'); + settings['color-scheme'] = newTheme; + updateButtonText(button, settings); + }); + }); + observer.observe(pre, { + attributes: true, + attributeFilter: ['data-prismjs-color-scheme'], + }); + } + + /** + * Create a color scheme button. + * + * @param {Object<string, any>} env - The environment variables of the hook. + * @returns {HTMLButtonElement} The color scheme button. + */ + function getColorSchemeButton(env) { + var element = env.element; + var pre = element.parentElement; + var settings = getSettings(element); + var themeButton = document.createElement('button'); + themeButton.className = 'prism-color-scheme-button'; + themeButton.setAttribute('type', 'button'); + updateButtonText(themeButton, settings); + updatePreAttribute(pre, settings['color-scheme']); + listenAttributeChange(pre, settings); + + themeButton.addEventListener('click', () => { + var newTheme = getNewTheme(settings['color-scheme']); + switchTheme(newTheme); + storage.set('prismjs-color-scheme', newTheme); + }); + + window.addEventListener('storage', (e) => { + if (e.key === 'prismjs-color-scheme') { + const newTheme = JSON.parse(e.newValue); + if (isValidTheme(newTheme)) updatePreAttribute(pre, newTheme); + } + }); + + return themeButton; + } + + /** + * Register a new button in Prism toolbar plugin. + */ + Prism.plugins.toolbar.registerButton('color-scheme', getColorSchemeButton); +})(); diff --git a/tsconfig.json b/tsconfig.json index dac9026..e110340 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,6 +27,11 @@ "@ts/*": ["src/ts/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "src/utils/plugins/prism-color-scheme.js" + ], "exclude": ["node_modules"] } |
