koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

Previmでダークモード対応する

背景

最近のWebサイトではOSのダークモード設定をそのまま反映しているものもある. 僕は普段からOSの設定をダークモードにしているので,previmでダークモード対応を行うことを考えた.

デモ

以下のようになった. ダークモードとライトモードの切り替えも示している.

Previmダークモードのデモ
Previmダークモードのデモ

実装方針

実装方針としては下記のようにした.

  • 本体はいじらずカスタムCSSとアドオンのみで対応する
  • ダークモードとライトモードを切り替え可能にする
    • このため,メディアクエリを用いたCSSだけでの対応は不可
    • 方法としてはプレビュー上にボタン追加やクエリパラメータではなく,開発者モードでの関数呼び出しでよい
      • applyLightTheme() でライトモードに, applyDarkThene() でダークモードに

実装

カスタムCSS

ライトモード,ダークモード用の色はCSS変数を用いて管理する. ライトモードの色はprevim付属のデフォルトのCSSに従う. ダークモードの色はGitHubの配色を参考に,デフォルトのCSSを置き換えることにした.

コードブロックの配色に関して,ライトモードはprevim付属のhighlight.jsのCSSではなく, ダークモードともども最新のhighlightjs/highlight.jsの下記CSSを用いることにした

モード CSS
ライトモード github.css
ダークモード github-dark.css

previmの仕組み上,付属のhighlight.jsのCSSはアドオンとして追加されるため,カスタムCSSより後に読み込まれる. そのため,CSSの優先順位を利用し,カスタムCSSの配色を優先させる.

/* Color for initial loading */
:root {
  --color-fg-normal: #333;
  --color-bg-normal: white;
  transition: 0.5s;
}
@media (prefers-color-scheme: dark) {
  :root {
    --color-fg-normal: #e6edf3;
    --color-bg-normal: #0d1117;
  }
}

/* Color for light mode */
:root.theme-light {
  --color-fg-normal: #333;
  --color-bg-normal: white;
  --color-bg-second: #f8f8f8;
  --color-fg-head: black;
  --color-border-code: #eaeaea;
  --color-blockquote: #777777;
  /* Colors from highlightjs/highlight.js/src/styles/github.css */
  --color-fg-hljs: #24292e;
  --color-bg-hljs: #f8f8f8;
  --color-fg-hljs-keyword: #d73a49;
  --color-fg-hljs-title: #6f42c1;
  --color-fg-hljs-variable: #005cc5;
  --color-fg-hljs-string: #032f62;
  --color-fg-hljs-symbol: #e36209;
  --color-fg-hljs-comment: #6a737d;
  --color-fg-hljs-name: #22863a;
  --color-fg-hljs-subst: #24292e;
  --color-fg-hljs-section: #005cc5;
  --color-fg-hljs-bullet: #735c0f;
  --color-fg-hljs-emphasis: #24292e;
  --color-fg-hljs-strong: #24292e;
  --color-fg-hljs-addition: #22863a;
  --color-bg-hljs-addition: #f0fff4;
  --color-fg-hljs-deletion: #b31d28;
  --color-bg-hljs-deletion: #ffeef0;
}

/* Color for dark mode */
:root.theme-dark {
  --color-fg-normal: #e6edf3;
  --color-bg-normal: #0d1117;
  --color-bg-second: #161b22;
  --color-fg-head: #e6edf3;
  --color-border-code: #111111;
  --color-blockquote: #e6edf3;
  /* Colors from highlightjs/highlight.js/src/styles/github-dark.css */
  --color-fg-hljs: #c9d1d9;
  --color-bg-hljs: #161b22;
  --color-fg-hljs-keyword: #ff7b72;
  --color-fg-hljs-title: #d2a8ff;
  --color-fg-hljs-variable: #79c0ff;
  --color-fg-hljs-string: #a5d6ff;
  --color-fg-hljs-symbol: #ffa657;
  --color-fg-hljs-comment: #8b949e;
  --color-fg-hljs-name: #7ee787;
  --color-fg-hljs-subst: #c9d1d9;
  --color-fg-hljs-section: #1f6feb;
  --color-fg-hljs-bullet: #f2cc60;
  --color-fg-hljs-emphasis: #c9d1d9;
  --color-fg-hljs-strong: #c9d1d9;
  --color-fg-hljs-addition: #aff5b4;
  --color-bg-hljs-addition: #033a16;
  --color-fg-hljs-deletion: #ffdcd7;
  --color-bg-hljs-deletion: #67060c;
}

html {
  background: var(--color-bg-normal);
}

body {
  color: var(--color-fg-normal);
}

h1 {
  color: var(--color-fg-head);
}

h2 {
  color: var(--color-fg-head);
}

pre,
.highlight pre {
  background-color: var(--color-bg-second);
}

code {
  border-color: var(--color-border-code);
  background-color: var(--color-bg-second);
}

blockquote {
  color: var(--color-blockquote);
}

table tr {
  background-color: var(--color-bg-normal);
}
table tr:nth-child(2n) {
  background-color: var(--color-bg-second);
}

/*
 * Overwrite higilight.css
 */
code.hljs {
  color: var(--color-fg-hljs);
  background: var(--color-bg-hljs);
}

span.hljs-doctag,
span.hljs-keyword,
span.hljs-meta .hljs-keyword,
span.hljs-template-tag,
span.hljs-template-variable,
span.hljs-type,
span.hljs-variable.language_ {
  /* prettylights-syntax-keyword */
  color: var(--color-fg-hljs-keyword);
}

span.hljs-title,
span.hljs-title.class_,
span.hljs-title.class_.inherited__,
span.hljs-title.function_ {
  /* prettylights-syntax-entity */
  color: var(--color-fg-hljs-title);
}

span.hljs-attr,
span.hljs-attribute,
span.hljs-literal,
span.hljs-meta,
span.hljs-number,
span.hljs-operator,
span.hljs-variable,
span.hljs-selector-attr,
span.hljs-selector-class,
span.hljs-selector-id {
  /* prettylights-syntax-constant */
  color: var(--color-fg-hljs-variable);
}

span.hljs-regexp,
span.hljs-string,
span.hljs-meta .hljs-string {
  /* prettylights-syntax-string */
  color: var(--color-fg-hljs-string);
}

span.hljs-built_in,
span.hljs-symbol {
  /* prettylights-syntax-variable */
  color: var(--color-fg-hljs-symbol);
}

span.hljs-comment,
span.hljs-code,
span.hljs-formula {
  /* prettylights-syntax-comment */
  color: var(--color-fg-hljs-comment);
}

span.hljs-name,
span.hljs-quote,
span.hljs-selector-tag,
span.hljs-selector-pseudo {
  /* prettylights-syntax-entity-tag */
  color: var(--color-fg-hljs-name);
}

span.hljs-subst {
  /* prettylights-syntax-storage-modifier-import */
  color: var(--color-fg-hljs-subst);
}

span.hljs-section {
  /* prettylights-syntax-markup-heading */
  color: var(--color-fg-hljs-section);
}

span.hljs-bullet {
  /* prettylights-syntax-markup-list */
  color: var(--color-fg-hljs-bullet);
}

span.hljs-emphasis {
  /* prettylights-syntax-markup-italic */
  color: var(--color-fg-hljs-emphasis);
}

span.hljs-strong {
  /* prettylights-syntax-markup-bold */
  color: var(--color-fg-hljs-strong);
}

span.hljs-addition {
  /* prettylights-syntax-markup-inserted */
  color: var(--color-fg-hljs-addition);
  background-color: var(--color-bg-hljs-addition);
}

span.hljs-deletion {
  /* prettylights-syntax-markup-deleted */
  color: var(--color-fg-hljs-deletion);
  background-color: var(--color-bg-hljs-deletion);
}

上記のCSS~/.vim/previm/custom.css として保存する.

アドオン設定

Previmの設定は下記のようにしておく. js のコードは url や path が無くても差し込めるとのこと.

アドオンの 'code' に記載したコードはプレビュー更新時に毎回実行されるものである. 今回実行したい処理はワンタイム実行のみでよく,本来は外部ファイルとすべきではあるが,めんどくさくてサボっている.

また,プレビュー初回読み込みまではモードなしとなるため,CSS側でデフォルトの配色設定を行っている. (特にダークモードについてはメディアクエリで前景色と背景色のCSS変数のみ設定している)

let g:previm_custom_css_path = '~/.vim/previm/custom.css'
let g:previm_extra_libraries = [
      \ {
      \   'name': 'theme',
      \   'files': [
      \     {
      \       'type': 'js',
      \       'code': [
      \         '(function(global, doc) {',
      \         "  'use strict';",
      \         '  function applyTheme(themeName) {',
      \         '    doc.documentElement.className = themeName;',
      \         '  }',
      \         "  if (doc.documentElement.className === '') {",
      \         "    var prefersColorSchemeDark = typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches;",
      \         "    applyTheme(prefersColorSchemeDark ? 'theme-dark' : 'theme-light');",
      \         '  }',
      \         '  global.applyLightTheme = function() {',
      \         "    applyTheme('theme-light');",
      \         '  };',
      \         '  global.applyDarkTheme = function() {',
      \         "    applyTheme('theme-dark');",
      \         '  };',
      \         '})(window, document);'
      \       ]
      \     }
      \   ]
      \ }
      \]

参考