「Alfasado Inc. / LAB Inc. Advent Calendar 2023」の第17日目です。普段の業務ではフロントエンドとサーバーサイド(PowerCMS Xプラグイン開発)のどちらも手掛けており、「PowerCMS Xのテンプレートのテストについて検討してみた」に書いたような研究調査活動もよくやっています。今日はフロントエンド寄りの話を書きたいと思います。
Webサイトリニューアル時に「HTMLImporterプラグイン」を利用して現在公開しているページをCMSに取り込む、という作業はよくあります。その際、現在公開しているページのコンテンツ部分をどう扱うかはいろいろパターンがあるかと思いますが、「現在公開しているページのCSSをそのまま適用する」とした場合に移行するCSSファイルの内容に苦しめられることがあります。例えば、要素セレクタを利用してスタイルが定義されている場合です。「これmain要素内に新たに掲載することになったコンポーネントにもスタイルが当たってしまうじゃん…」と。
CSSファイルが少なければ手で編集して対応することもできるのですが、HTMLImporterプラグインを利用するケースではたいていページ数がかなりあり、かつページ単位でCSSファイルを用意しているケースもあるので、変更しなければならないCSSファイル数もかなりの数になることがあります。(なお、これは実話です。)
「これはどうしたものか…」と悩んだのですが、実際にしなければならないことは「移行するCSSファイルで定義されているスタイルの適用範囲をエディタの部分に限定すること」しかありません。そこでひらめいたのが「PostCSSを利用してCSSセレクタにクラス名(.p-migrateContent
)を足そう」ということです。「Autoprefixer」のようなプラグインがあるからきっと何とかなるはず、と考えました。
そしてできあがったPostCSSのプラグインがこの後掲載するコードです。
main
だけのセレクタの場合、main
を.p-migrateContent
に置き変えるmain
から始まるセレクタの場合、main
の後ろに.p-migrateContent
を足す- 要素セレクタの頭に
.p-migrateContent
を足す header
・aside
・footer
に対する定義は削除する
といった処理をしています。要素セレクタのみに.p-migrateContent
を足すと意図しない詳細度(Specificity)になって崩れる可能性を避けるために、IDを含まないセレクタには全て.p-migrateContent
を足してみました。
const plugin = () => {
return {
postcssPlugin: 'postcss-add-class-prefix',
Rule(rule) {
const addSelector = '.p-migrateContent';
if (
rule.selector.indexOf(addSelector) !== -1 ||
rule.selector.indexOf('#') !== -1 ||
rule.selector.indexOf('%') !== -1
) {
return;
} else if (/^(header|aside|footer)$/.test(rule.selector)) {
rule.remove();
}
let modifiedSelector = '';
if (/(^|\s)main$/.test(rule.selector)) {
modifiedSelector = rule.selector.replace(/main$/ugi, addSelector);
} else if (/(^|\s)main\s/.test(rule.selector)) {
modifiedSelector = rule.selector.replace(/main\s/ugi, `main ${addSelector} `);
} else if (rule.selector.indexOf(',') !== -1) {
modifiedSelector = rule.selectors.map((part) => {
if (
part.indexOf(addSelector) === -1 &&
part.indexOf('#') === -1
) {
return `${addSelector} ${part}`;
}
return part;
}).join(', ');
} else if (!/(^|\s)to$/.test(rule.selector)) {
modifiedSelector = `${addSelector} ${rule.selector}`;
} else {
modifiedSelector = rule.selector;
}
rule.selector = modifiedSelector;
}
}
};
plugin.postcss = true;
module.exports = plugin;
このコード(PostCSSプラグイン)は以下のようにpostcss.config.jsを記述し、npx postcss src/common/css/style.css --dir dist/common/css/ --config postcss.config.js
を実行することで動作します。
module.exports = {
plugins: [
require('./postcss-add-class-prefix.js')
]
}
処理前後のCSSファイルの差分を表示すると以下のようになります。
複数ファイルをまとめて処理するには、recursive-readdir-syncを利用してファイルを読み込みPostCSSプラグインを実行していきます。サンプルコードを示します。(もしかするとChatGPTに書いてもらったかも。)
const postcss = require('postcss');
const fs = require('fs');
const path = require('path');
const recursiveReadSync = require('recursive-readdir-sync');
const inputDir = 'src'; // 入力ディレクトリ
const outputDir = 'dist'; // 出力ディレクトリ
const files = recursiveReadSync(inputDir);
files.forEach(file => {
const inputFile = file;
const relativePath = path.relative(inputDir, inputFile);
const outputFile = path.join(outputDir, relativePath);
const css = fs.readFileSync(inputFile, 'utf8');
console.log(file);
postcss([
require('./postcss-add-class-prefix.js')
])
.process(css, { from: inputFile, to: outputFile })
.then(result => {
fs.mkdirSync(path.dirname(outputFile), { recursive: true }); // 出力ディレクトリが存在しない場合、再帰的にディレクトリを作成
fs.writeFileSync(outputFile, result.css);
if (result.map) {
fs.writeFileSync(`${outputFile}.map`, result.map);
}
})
.catch(error => console.error(error));
});
開発にあたってはドキュメント「Writing a PostCSS Plugin」が参考になりました。この記事がHTMLImporterプラグインを利用したページ移行作業時のヒントになれば幸いです。
ちなみに、CSS Cascading and Inheritance Level 6の「Scoping Styles」の実装が進めば今回のような作業(エディタ部分のスタイル適用)はかなり楽になると思います。