aboutsummaryrefslogtreecommitdiffstats
path: root/src/utils/plugins/prism-color-scheme.js
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
(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);
})();