HTMLでもcss propを使いたい
みなさんはCSSライブラリ何を使われてますでしょうか。
私はemotionのcss propが好みでよく使ってます。
- スタイルに自動でハッシュを付与してくれるのでclassの命名に無駄な労力を割く必要がない。
- HTML要素のそばにスタイルを書けるのでコンポーネントとしての見通しがよい。
あたりが好きなところです。
ただし、当然ながらemotionはCSS-in-JSのライブラリですので、生のHTMLやThymeleafなどのテンプレートエンジンでは (そのままでは) 使用できません。HTMLでもcss propを使いたいなーと思ったタイミングがあり少し探していたのですが、特に見当たらなかったため自分で作っちゃおうということでPostHTMLのプラグインとして動作するemotionのラッパーライブラリ: posthtml-css-prop
を実装しました。
本ライブラリは、このように生HTMLに対してcss propのような処理をサポートします。
<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>
<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>
ここでは処理の概要をご説明しようと思います。
-
PostHTML Pluginのスケルトン
公式ガイドがあるのでそれにしたがって実装していきます。PostHTML PluginはJavaScriptで実装されているものが多いのですが、どうしてもTypeScriptを使いたかったのでTypeScript用のボイラープレートを使用しました。
また、バンドルはrollupを使用しています。 -
各要素の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/css
のcss()
を使用してclass名を自動生成しています。
また、ソースコードを見ればわかるように、css()
はCSSからclass名を自動生成するだけでなく生成したclass名とCSSを紐付けてcache
に登録しています。
-
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