aboutsummaryrefslogtreecommitdiffstats
path: root/src/utils/plugins/prism-color-scheme.cjs
blob: 2632dd33f2266a2c1a33792d44c3a75bf173a690 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
(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<"current", Theme> & Record<"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 = {
      current: setDefaultTheme(),
      dark: 'Toggle Dark Theme',
      light: 'Toggle Light Theme',
    };
    var prefix = 'data-prismjs-color-scheme-';

    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 {Theme} theme - The current theme.
   * @param {Settings} settings - The plugin settings.
   * @returns {string} The button text.
   */
  function getButtonContent(theme, settings) {
    return theme === 'dark' ? settings['light'] : 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 theme =
      settings['current'] === 'system'
        ? getThemeFromSystem()
        : settings['current'];

    button.textContent = getButtonContent(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-current', 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-current]'
    );
    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-current'
        );
        settings['current'] = newTheme;
        updateButtonText(button, settings);
      });
    });
    observer.observe(pre, {
      attributes: true,
      attributeFilter: ['data-prismjs-color-scheme-current'],
    });
  }

  /**
   * 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['current']);
    listenAttributeChange(pre, settings);

    themeButton.addEventListener('click', () => {
      var newTheme = getNewTheme(settings['current']);
      switchTheme(newTheme);
      storage.set('prismjs-color-scheme', newTheme);
    });

    window.addEventListener('storage', (e) => {
      if (e.key === 'prismjs-color-scheme') {
        var 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);
})();