style-modは、スタイルを動的に管理するためのJavaScriptライブラリ、いわゆるCSS-in-JSの一種である。作者は、CodeMirror6と同じMarijn Haverbeke氏。CodeMirror6でのエディタのスタイリングにも、このstyle-modが使われている。

style-modの使い方

style-modは、@codemirror/viewの依存関係に含まれている。CodeMirror6のエディタをスタイリングするぶんには、その記法だけ知っていればいい。

単独で使いたい場合は、手動でnpmパッケージをインストールするか、CDNを使う。

npmパッケージを使う場合

bash
npm i -D style-mod
js
import { StyleModule } from "style-mod";
// コード

CDNを使う場合

style-modのホスティングを確認できたのがjsDelivrのみだったため、差し当たりこれをサンプルに挙げる(目をザルのようにして探したので、他にもあるかも知れない)。

html
<script type="module">
  import { StyleModule } from "https://cdn.jsdelivr.net/npm/style-mod@4.1.0/+esm";
  // コード
</script>

使い方は以下の通り至ってシンプルである。

  1. StyleModuleコンストラクタにstyle-modの記法でCSSルールを渡す。
  2. StyleModule.mount()メソッドで任意のDocument、あるいはShadowRootにマウントする。

style-mod記法

記法と言っても、CSSをJavaScriptオブジェクトに置き換えるシンプルなものである。

キーには、文字列だけでなくJavaScriptの識別子も使える。この場合、CSSのプロパティ名はキャメルケースに直す必要がある(例えばfont-sizefontSizeとする)。

また、SASSのように、&は現在のセレクタとして扱える。トップレベルで&を指定すると、そのドキュメントがセレクトされる。

この記法で書いたCSSをもとにStyleModuleオブジェクトを作り、mount()メソッドで任意のDocument、あるいはShadowRootにスタイルを反映するのが、style-modの基本的な使い方である。

サンプルコード

html
<script type="module">
  import {StyleModule} from 'https://cdn.jsdelivr.net/npm/style-mod@4.1.0/+esm'

  const myMod = new StyleModule({
    "&":{
      fontFamily:"serif",
    },
    "p":{
      fontSize:"16px",
      color:"green",
      "&:first-of-type":{
        "font-size":"20px", //文字列でそのままプロパティ名を書いてもいい。
        fontWeight:"bold",
        "&:hover": {
          "color": "orange"
        }
      }
    },
    "@media (min-width: 768px)":{
      "p":{
        color:"gray"
      }
    }
  });
  StyleModule.mount(document, myMod);

  document.body.innerHTML = `<p>1つ目のpタグ(ホバーするとオレンジ色になるよ)</p><p>2つ目のpタグ<p>`
</script>

上記コードの場合、ドキュメントの<head>タグに以下の内容の<style>タグが挿入される。

css
& {
  font-family: serif;
}
p:first-of-type:hover {
  color: orange;
}
p:first-of-type {
  font-size: 20px;
  font-weight: bold;
}
p {
  font-size: 16px;
  color: green;
}
@media (min-width: 768px) {
  p {
    color: gray;
  }
}

指定した通りのものがそのまま反映されていることがわかる。

style_mod記法の留意点

シンプルな記法だけに留意点はほぼない。ただ、プロパティ名にアンダースコアが入っている場合、アンダースコア含めて以降の文字列は削除される。ドキュメントによると、この仕様はブラウザの互換性のために、プロパティを複数回提供する場合等に使えるらしい。

If you include an underscore in a property name, it and everything after it will be removed from the output, which can be useful when providing a property multiple times, for browser compatibility reasons.

引用元:marijnh/style-mod

私には使い方がよくわからない。StyleModuleクラスのコンストラクタに渡すfinish()関数とコンボで使うのではないかと思う。

StyleModuleクラスの概要

参考までにStyleModuleクラスの概要をまとめておく。

ts
class StyleModule {
  /* オプションのfinish関数では、各セレクタを引数として受け取り、任意の変更ができる。 */
  constructor(spec: Object< Style >, options: ?{finish: ?fn(string)string})

  /* コンストラクタに渡した記法をCSSルールに変換した文字列を返す */
  getRules():string

  /* ユニークなCSS名を返す。具体的には、「ͼ1」のように、「ͼ+newName()メソッドが呼び出された数」である */
  static newName():string

  /* DocumentあるいはShadowRootにCSSルールを挿入する。nonce値を指定したい時は第3引数を使う。*/
  static mount(root: Document | ShadowRoot, modules: [StyleModule] | StyleModule, options: ?{nonce: ?string})
}

style-modのソースコード

style-modはミニマルである。詳しく知りたいのであれば、ソースコードを読んだ方が早い。

style-modのソースコード

js
const C = "\u037c";
const COUNT = typeof Symbol == "undefined" ? "__" + C : Symbol.for(C);
const SET =
  typeof Symbol == "undefined"
    ? "__styleSet" + Math.floor(Math.random() * 1e8)
    : Symbol("styleSet");
const top =
  typeof globalThis != "undefined"
    ? globalThis
    : typeof window != "undefined"
    ? window
    : {};

export class StyleModule {
  constructor(spec, options) {
    this.rules = [];
    let { finish } = options || {};

    function splitSelector(selector) {
      return /^@/.test(selector) ? [selector] : selector.split(/,\s*/);
    }

    function render(selectors, spec, target, isKeyframes) {
      let local = [],
        isAt = /^@(\w+)\b/.exec(selectors[0]),
        keyframes = isAt && isAt[1] == "keyframes";
      if (isAt && spec == null) return target.push(selectors[0] + ";");
      for (let prop in spec) {
        let value = spec[prop];
        if (/&/.test(prop)) {
          render(
            prop
              .split(/,\s*/)
              .map((part) => selectors.map((sel) => part.replace(/&/, sel)))
              .reduce((a, b) => a.concat(b)),
            value,
            target
          );
        } else if (value && typeof value == "object") {
          if (!isAt)
            throw new RangeError(
              "The value of a property (" +
                prop +
                ") should be a primitive value."
            );
          render(splitSelector(prop), value, local, keyframes);
        } else if (value != null) {
          local.push(
            prop
              .replace(/_.*/, "")
              .replace(/[A-Z]/g, (l) => "-" + l.toLowerCase()) +
              ": " +
              value +
              ";"
          );
        }
      }
      if (local.length || keyframes) {
        target.push(
          (finish && !isAt && !isKeyframes
            ? selectors.map(finish)
            : selectors
          ).join(", ") +
            " {" +
            local.join(" ") +
            "}"
        );
      }
    }

    for (let prop in spec) render(splitSelector(prop), spec[prop], this.rules);
  }

  getRules() {
    return this.rules.join("\n");
  }

  static newName() {
    let id = top[COUNT] || 1;
    top[COUNT] = id + 1;
    return C + id.toString(36);
  }

  static mount(root, modules, options) {
    let set = root[SET],
      nonce = options && options.nonce;
    if (!set) set = new StyleSet(root, nonce);
    else if (nonce) set.setNonce(nonce);
    set.mount(Array.isArray(modules) ? modules : [modules]);
  }
}

let adoptedSet = new Map();

class StyleSet {
  constructor(root, nonce) {
    let doc = root.ownerDocument || root,
      win = doc.defaultView;
    if (!root.head && root.adoptedStyleSheets && win.CSSStyleSheet) {
      let adopted = adoptedSet.get(doc);
      if (adopted) {
        root.adoptedStyleSheets = [adopted.sheet, ...root.adoptedStyleSheets];
        return (root[SET] = adopted);
      }
      this.sheet = new win.CSSStyleSheet();
      root.adoptedStyleSheets = [this.sheet, ...root.adoptedStyleSheets];
      adoptedSet.set(doc, this);
    } else {
      this.styleTag = doc.createElement("style");
      if (nonce) this.styleTag.setAttribute("nonce", nonce);
      let target = root.head || root;
      target.insertBefore(this.styleTag, target.firstChild);
    }
    this.modules = [];
    root[SET] = this;
  }

  mount(modules) {
    let sheet = this.sheet;
    let pos = 0 /* Current rule offset */,
      j = 0; /* Index into this.modules */
    for (let i = 0; i < modules.length; i++) {
      let mod = modules[i],
        index = this.modules.indexOf(mod);
      if (index < j && index > -1) {
        // Ordering conflict
        this.modules.splice(index, 1);
        j--;
        index = -1;
      }
      if (index == -1) {
        this.modules.splice(j++, 0, mod);
        if (sheet)
          for (let k = 0; k < mod.rules.length; k++)
            sheet.insertRule(mod.rules[k], pos++);
      } else {
        while (j < index) pos += this.modules[j++].rules.length;
        pos += mod.rules.length;
        j++;
      }
    }

    if (!sheet) {
      let text = "";
      for (let i = 0; i < this.modules.length; i++)
        text += this.modules[i].getRules() + "\n";
      this.styleTag.textContent = text;
    }
  }

  setNonce(nonce) {
    if (this.styleTag && this.styleTag.getAttribute("nonce") != nonce)
      this.styleTag.setAttribute("nonce", nonce);
  }
}

引用元:style-modのGitHubリポジトリ

参考資料