diff options
Diffstat (limited to 'src/utils/plugins/prism-color-scheme.js')
| -rw-r--r-- | src/utils/plugins/prism-color-scheme.js | 252 | 
1 files changed, 252 insertions, 0 deletions
| 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); +})(); | 
