👩‍🎤

HTMLでもcss propを使いたい

2022/04/30に公開

みなさんはCSSライブラリ何を使われてますでしょうか。

私はemotionのcss propが好みでよく使ってます。

  • スタイルに自動でハッシュを付与してくれるのでclassの命名に無駄な労力を割く必要がない。
  • HTML要素のそばにスタイルを書けるのでコンポーネントとしての見通しがよい。

あたりが好きなところです。

ただし、当然ながらemotionはCSS-in-JSのライブラリですので、生のHTMLやThymeleafなどのテンプレートエンジンでは (そのままでは) 使用できません。HTMLでもcss propを使いたいなーと思ったタイミングがあり少し探していたのですが、特に見当たらなかったため自分で作っちゃおうということでPostHTMLのプラグインとして動作するemotionのラッパーライブラリ: posthtml-css-propを実装しました。
https://github.com/shimech/posthtml-css-prop

本ライブラリは、このように生HTMLに対してcss propのような処理をサポートします。

input.html
<html>
  <head></head>
  <body>
    <h1 css-prop="text-align: center; font-size: 24px;">Title</h1>
    <div class="foo" css-prop="display: flex;">
      <span css-prop="color: red; &:hover { color: blue; }">Hello World!</span>
    </div>
  </body>
</html>
output.html
<html>
  <head>
    <style data-posthtml-css-prop="css 1pwdwr4">
      .css-1pwdwr4 {
        text-align: center;
        font-size: 24px;
      }
    </style>
    <style data-posthtml-css-prop="css 1q8jsgx">
      .css-1q8jsgx {
        display: -webkit-box;
        display: -webkit-flex;
        display: -ms-flexbox;
        display: flex;
      }
    </style>
    <style data-posthtml-css-prop="css qrwk6l">
      .css-qrwk6l {
        color: red;
      }
      .css-qrwk6l:hover {
        color: blue;
      }
    </style>
  </head>
  <body>
    <h1 class="css-1pwdwr4">Title</h1>
    <div class="css-1q8jsgx foo">
      <span class="css-qrwk6l">Hello World!</span>
    </div>
  </body>
</html>

ここでは処理の概要をご説明しようと思います。

  1. PostHTML Pluginのスケルトン
    公式ガイドがあるのでそれにしたがって実装していきます。PostHTML PluginはJavaScriptで実装されているものが多いのですが、どうしてもTypeScriptを使いたかったのでTypeScript用のボイラープレートを使用しました。
    また、バンドルはrollupを使用しています。

  2. 各要素のcss propの収集

tree.match<StringMatcher, { "css-prop"?: RegExp }>(
  { attrs: { "css-prop": /\w+/ } },
  (node) => {
    const { "css-prop": style, ...prevAttrs } = node.attrs;
    if (!style) {
      return node;
    }
    const className = css`
      ${style}
    `;
    return {
      ...node,
      attrs: {
        ...prevAttrs,
        class: prependClass(prevAttrs.class, className),
      },
    };
  },
);

PostHTMLから提供されているtree.match()というAPIを使用することで、該当する要素を探しながらHTMLの抽象構文木をtraverseすることができます。本ライブラリでは、css-propという属性を持つ要素を抽出し、@emotion/csscss()を使用してclass名を自動生成しています。
また、ソースコードを見ればわかるように、css()はCSSからclass名を自動生成するだけでなく生成したclass名とCSSを紐付けてcacheに登録しています。

  1. headタグにstyleタグを追加
tree.match({ tag: "head" }, (node) => {
  const prevContent = node.content || [];
  const styles = Object.entries(cache.inserted).map(
    ([id, css]) =>
      parser(
        generateStyleTag(cache.key, id, css as string),
      ) as unknown as Node,
  );
  return {
    ...node,
    content: [...prevContent, ...styles],
  };
});

まず、先程と同様にtree.match()でtraverseしながら、今度はheadタグを抽出します。
そして、先程のcacheを使用してstyleタグを生成し、headタグに追加しています。
(一部型注釈をごまかしているのは土下座案件です。)

処理は以上です。
ほとんどemotionに処理を任せているため非常に簡単に実装できました。

最後に、本ライブラリの使用方法です。

const posthtml = require("posthtml");
const html = `
  <html>
    <head></head>
    <body>
      <h1 css-prop="text-align: center; font-size: 24px;">Title</h1>
      <div class="foo" css-prop="display: flex;">
        <span css-prop="color: red; &:hover { color: blue; }">Hello World!</span>
      </div>
    </body>
  </html>
`;

posthtml()
  .use(require("@shimech/posthtml-css-prop")())
  .process(html)
  .then((result) => console.log(result.html));
// Output:
// <html>
//     <head><style data-posthtml-css-prop="css 1pwdwr4">.css-1pwdwr4{text-align:center;font-size:24px;}</style><style data-posthtml-css-prop="css 1q8jsgx">.css-1q8jsgx{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}</style><style data-posthtml-css-prop="css qrwk6l">.css-qrwk6l{color:red;}.css-qrwk6l:hover{color:blue;}</style></head>
//     <body>
//         <h1 class="css-1pwdwr4">Title</h1>
//         <div class="css-1q8jsgx foo">
//             <span class="css-qrwk6l">Hello World!</span>
//         </div>
//     </body>
// </html>

個人的には、これを実装することでemotionの内部処理に詳しくなれた (cache持ってるのとか知らなかった) のでいい勉強になりました。
最後までお読みいただきありがとうございました。

Discussion