tailwindcss と bootstrap の共存のために
bootstrap 5 で 作成したページの中に tailwindcss で作成した
部品を使いたくなって
tailwindcss には tailwind.config.js
に
module.exports = {
prefix: 'tw-',
という定義を書くと
<div class="tw-px-8 tw-mx-auto">
</div>
のように すべてのクラスに tw-
をつけて
Bootstrap 5 の サイト内に持ち込み可能だと判明した。
よしがんばって 取り込むぞ、と思ったけど あまりに数が多くて「大変だな・・これは・・」と思いました。
Github Copilot の出番
ああそうだ、こんなめんどくさいプログラムは Github Copilot
にお願いしよう。
コマンドプロンプトで
mkdir tailwind-css-change
cd tailwind-css-change
dotnet new console
code .
開いた Program.cs
ファイルに
// 入力ファイル
string inHtml = "../tailwind-bs5/wwwroot/html/page1.html";
string outHtml = "../tailwind-bs5/wwwroot/html/page1_fix.html";
を書いて
html ファイルを 読み込み HTML として 解析して
その中に含まれる class を取り出し
class を 文字列の配列に置き換え
それぞれの class の 文字列の先頭に tw-
をつけて class を 置き換える。
このコードを C# で Html Agility Pack を使って 書いて
とお願いしたら
下記のような回答をもらった。
まず、Html Agility Packをプロジェクトに追加する必要があります。
これは、NuGetパッケージマネージャを使用して行うことができます。
dotnet add package HtmlAgilityPack
using System;
using System.IO;
using HtmlAgilityPack;
class Program
{
static void Main(string[] args)
{
string inHtml = "../tailwind-bs5/wwwroot/html/page1.html";
string outHtml = "../tailwind-bs5/wwwroot/html/page1_fix.html";
// HtmlAgilityPack を使用して HTML ファイルを読み込む
HtmlDocument doc = new HtmlDocument();
doc.Load(inHtml);
// すべての要素を走査
foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//*[@class]"))
{
// class 属性の値を取得
string classes = node.GetAttributeValue("class", "");
// 空白で分割して配列にする
string[] classList = classes.Split(' ');
// 各クラス名の先頭に "tw-" を追加
for (int i = 0; i < classList.Length; i++)
{
classList[i] = "tw-" + classList[i];
}
// 変更されたクラス名を元に戻す
node.SetAttributeValue("class", String.Join(" ", classList));
}
// 変更を加えた HTML を新しいファイルに保存
doc.Save(outHtml);
}
}
すごい、ほぼ完ぺきに html の置き換えができた。
たくさんの HTMLがあったから かなりの時間節約になった・・。
動作確認したら hover:bg-blue-500
がちゃんと動いてない事を発見。
tailwindcss 公式サイト prefix の説明部分
を確認したら
<div class="tw-text-lg md:tw-text-xl tw-bg-red-500 hover:tw-bg-blue-500">
のように sm:
や hover:
の 場合は :
の後ろに tw-
をつけるらしい。
また -mt-8
の場合には -tw-mt-8
のように書くらしい。
Copilot に次のお願い
tw- を class の先頭につける場合 class の中に : が含まれていたら : の前と後に分割して
後の文字列の前に tw- をつけて文字列を生成して。
例えば sm:bg-blue-500 の場合は sm:tw-blue-500 のように。
また class が - (マイナス記号)で始まる場合は -tw をつけて。
例えば -mt-8 の場合には -tw-mt-8 のように。
そうしたら 次の回答
クラス名に : が含まれている場合に : の前後に分割し、
後ろの文字列の前に tw- をつける処理と、
クラス名が - で始まる場合に -tw をつける処理を
組み合わせたコードを以下に示します。
for (int i = 0; i < classList.Length; i++)
{
// クラス名が "-" で始まる場合
if (classList[i].StartsWith("-"))
{
classList[i] = "-tw" + classList[i].Substring(1);
}
// クラス名に ":" が含まれている場合
else if (classList[i].Contains(":"))
{
// 先頭の ":" でのみ分割する
string[] parts = classList[i].Split(new char[] { ':' }, 2);
// 後ろ側の文字列の前に "tw-" を追加
classList[i] = parts[0] + ":tw-" + parts[1];
}
// それ以外の場合、先頭に "tw-" を追加
else
{
classList[i] = "tw-" + classList[i];
}
}
このコードでは、まずクラス名が - で始まるかどうかを StartsWith("-") でチェックし、
その場合は先頭に -tw を追加します。
クラス名に : が含まれている場合は、Split メソッドを使用して
先頭の : でのみ分割し、
後ろ側の文字列の前に tw- を追加します。
どちらの条件にも当てはまらない場合は、
単純にクラス名の先頭に tw- を追加します。
かなり高精度に 置き換えができている。
あれ? もし hover:-mt-8
の場合はどうなる?
実験してみた結果 hover:-tw-mt-8
となることが分かった。
次のお願い。
class に tw- をつける処理で 追加の条件があります。
文字列の中に : が含まれて なおかつ : の後ろ側の文字が - (マイナス記号) で始まる場合には
: で 文字列を分割した後ろ側の文字の先頭に -tw を加えてください。
その回答が以下
for (int i = 0; i < classList.Length; i++)
{
// クラス名が "-" で始まる場合
if (classList[i].StartsWith("-"))
{
classList[i] = "-tw" + classList[i].Substring(1);
}
// クラス名に ":" が含まれている場合
else if (classList[i].Contains(":"))
{
// 先頭の ":" でのみ分割する
string[] parts = classList[i].Split(new char[] { ':' }, 2);
// ":" の後ろ側の文字が "-" で始まる場合
if (parts[1].StartsWith("-"))
{
// 後ろ側の文字列の先頭に "-tw" を追加
classList[i] = parts[0] + ":-tw" + parts[1].Substring(1);
}
else
{
// 後ろ側の文字列の前に "tw-" を追加
classList[i] = parts[0] + ":tw-" + parts[1];
}
}
// それ以外の場合、先頭に "tw-" を追加
else
{
classList[i] = "tw-" + classList[i];
}
}
ああ、階層が増えてきたので tw を加える部分を 改造しようかなと思い、次の指示
class にtw 文字列を付け加える処理を見直します。
1)tw を付加する関数 文字列の先頭が - (マイナス)で始まる場合は -tw をつける。
それ以外は tw- をつける。
2)class の文字列に : が含まれている場合は : の前後で文字列を分割して
後ろ側の文字に 1) の tw を付加する処理を行う。
: が含まれていない場合は
class 文字列全体に 1) の tw を付加する処理を行う。
例えば
mt-8 は tw-mt-8 に
-mt-8 は -tw-mt-8 に
hover:mt-8 は hover-tw-mt-8 に
hover:-mt-8 は hover:-tw-mt-8 に なります。
その Github Copilot
の回答が以下
// 1) tw を付加する関数
string AddTwPrefix(string className)
{
if (className.StartsWith("-"))
{
return "-tw" + className.Substring(1);
}
else
{
return "tw-" + className;
}
}
// 2) class の文字列を処理する関数
string ProcessClassName(string className)
{
if (className.Contains(":"))
{
// ":" の前後で文字列を分割
var parts = className.Split(new[] { ':' }, 2);
// 後ろ側の文字に AddTwPrefix 関数を適用
return parts[0] + ":" + AddTwPrefix(parts[1]);
}
else
{
// class 文字列全体に AddTwPrefix 関数を適用
return AddTwPrefix(className);
}
}
// 使用例
Console.WriteLine(ProcessClassName("mt-8")); // 出力: tw-mt-8
Console.WriteLine(ProcessClassName("-mt-8")); // 出力: -tw-mt-8
Console.WriteLine(ProcessClassName("hover:mt-8")); // 出力: hover:tw-mt-8
Console.WriteLine(ProcessClassName("hover:-mt-8")); // 出力: hover:-tw-mt-8
コメントが 簡潔で 必要な場所にさりげなくついていて
関数名も勝手に考えてくれて きれいな処理になっているし。
いやーーすごいな・・と感心して コードを実行したら
tw-mt-8
-twmt-8
hover:tw-mt-8
hover:-twmt-8
あれ?? ちがうよね。-mt-8
の処理が間違ってるよ。
具体例を出して認識齟齬しないように指示したつもりなのに。
マイナスという記号を取り除くという勘違いがあったようですね。
AddTwPrefix
関数の
return "-tw" + className.Substring(1);
は
return "-tw" + className;
ですね。
最後に
inDir 配下にある html ファイルを HtmlReplace 関数を呼び出して
HTML の中身を置き換えて ファイル名に _fix をつけて
同じディレクトリ保存するコードを書いてください。
とお願いして 生成されたコードが 下記。
using System;
using System.IO;
using System.Linq;
class Program
{
static void Main(string[] args)
{
string inDir = "../tailwind-bs5/wwwroot/html"; // 入力ディレクトリ
ReplaceHtmlFiles(inDir);
}
static void ReplaceHtmlFiles(string directoryPath)
{
// 指定ディレクトリ内のすべての .html ファイルを取得
var htmlFiles = Directory.GetFiles(directoryPath, "*.html", SearchOption.TopDirectoryOnly);
foreach (var filePath in htmlFiles)
{
// ファイルの内容を読み込む
string content = File.ReadAllText(filePath);
// HtmlReplace 関数を使用して内容を置き換える
string replacedContent = HtmlReplace(content);
// 新しいファイル名を生成 (_fix を付ける)
string newFilePath = Path.Combine(Path.GetDirectoryName(filePath), Path.GetFileNameWithoutExtension(filePath) + "_fix" + Path.GetExtension(filePath));
// 置き換えた内容を新しいファイルに保存
File.WriteAllText(newFilePath, replacedContent);
}
}
// HTML の中身を置き換える関数 (ダミー実装)
static string HtmlReplace(string htmlContent)
{
// ここに HTML 内容を置き換える実際のロジックを実装
return htmlContent; // 仮実装
}
}
ファイル名に _fix をつけて
の部分を誤解するかな?
と思ったけどちゃんと input.html を input_fix.html に変換している。
今までに作成した関数をすべてマージして出来上がった最終コードが下記。
using HtmlAgilityPack;
class Program
{
static void Main(string[] args)
{
string inDir = "../tailwind-bs5/wwwroot/html"; // 入力ディレクトリ
ReplaceHtmlFiles(inDir);
}
static void ReplaceHtmlFiles(string directoryPath)
{
// 指定ディレクトリ内のすべての .html ファイルを取得
var htmlFiles = Directory.GetFiles(directoryPath, "*.html", SearchOption.TopDirectoryOnly);
foreach (var filePath in htmlFiles)
{
// HtmlReplace 関数を使用して内容を置き換える
string replacedContent = HtmlReplace(filePath);
// 新しいファイル名を生成 (_fix を付ける)
string newFilePath = Path.Combine(Path.GetDirectoryName(filePath), Path.GetFileNameWithoutExtension(filePath) + "_fix" + Path.GetExtension(filePath));
// 置き換えた内容を新しいファイルに保存
File.WriteAllText(newFilePath, replacedContent);
}
}
// HTML の中身を置き換える関数
static string HtmlReplace(string inHtmlFile)
{
// HtmlAgilityPack を使用して HTML ファイルを読み込む
HtmlDocument doc = new HtmlDocument();
doc.Load(inHtmlFile);
// すべての要素を走査
foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//*[@class]"))
{
// class 属性の値を取得
string classes = node.GetAttributeValue("class", "");
// 空白で分割して配列にする
string[] classList = classes.Split(' ');
// 各クラス名の先頭に "tw-" を追加
for (int i = 0; i < classList.Length; i++)
{
// class の文字列を処理する関数
classList[i] = ProcessClassName(classList[i]);
}
// 変更されたクラス名を元に戻す
node.SetAttributeValue("class", String.Join(" ", classList));
}
// 変更を加えた HTML 文字列を応答する
return doc.DocumentNode.OuterHtml;
}
// 1) tw を付加する関数
static string AddTwPrefix(string className)
{
if (className.StartsWith("-"))
{
return "-tw" + className;
}
else
{
return "tw-" + className;
}
}
// 2) class の文字列を処理する関数
static string ProcessClassName(string className)
{
if (className.Contains(":"))
{
// ":" の前後で文字列を分割
var parts = className.Split(new[] { ':' }, 2);
// 後ろ側の文字に AddTwPrefix 関数を適用
return parts[0] + ":" + AddTwPrefix(parts[1]);
}
else
{
// class 文字列全体に AddTwPrefix 関数を適用
return AddTwPrefix(className);
}
}
}
単純にコピー&ペーストしたらエラーが出たので
static 宣言に変更したり、HTML は 直接ファイルから読み込むように修正して実行。
dotnet add package HtmlAgilityPack
dotnet run
無事に スタイルのクラス名に tw-
がついた HTML が生成されました。
追記
sm:hover:bg-200
の 場合には
sm:hover:tw-bg-200
のように書くようです。
Github Copilot に訂正のお願い。
前に書いた ProcessClassName 関数で
class 文字列を : で 分解して tw を付加する処理の修正をお願いします。
: が複数含まれている場合 : で 複数の文字列に分割した後 最後の文字の先頭に
tw- を付加するようなコードに変更してください。
その回答が以下
// 2) class の文字列を処理する関数
static string ProcessClassName(string className)
{
// ":" で文字列を分割
var parts = className.Split(':');
if (parts.Length > 1)
{
// 最後の文字列に AddTwPrefix 関数を適用
parts[parts.Length - 1] = AddTwPrefix(parts[parts.Length - 1]);
// 分割された文字列を ":" で結合
return string.Join(":", parts);
}
else
{
// class 文字列全体に AddTwPrefix 関数を適用
return AddTwPrefix(className);
}
}
まとめ
Github Copilot
を使って対話的にコード生成した時の例を書きました。
作業内容をタスクに分類してみましたが、★1,★3、★4 の部分は プログラム作成の 急所であり、まだ 生成AI ででは代用できないようです。
コードでやりたい事と、その具体的な方法の概要が見えている。(★1)
おおざっぱに手順を指示すると それを実現するプログラムコードを生成してくれる(★2)
そのコードをテストする(★3)
予想と違う動きをしたので調査(★4)
コード生成の条件を変更を指示する(★5)
生成AI が 新しい指示でコードを生成する(★6)
生成したコードを動かして再度、動作確認(★7)
作成したコードを今回限りで捨てるか、再利用するのか、
誰でも使えるように体裁を整えるのか、今後のメンテナンス方針を検討する。(★8 今回は WEBに公開して完了。自分も将来もしかすると使う事があるかも。)
すべての仕様を網羅しているか、いろいろなバリエーションで正しく動作するのか?
ちゃんときっちり仕様と照らし合わせてバリエーションテストを行う。(★9 未実施。WEB に公開する事で誰かが不具合を発見して報告してくれる事を期待)
あとがき
そのうち 生成AI に 「私は ホームページは得意です。いくつかの質問に回答するだけで素敵なページを作成できます。」と言い出す時が来るのかな。
そんな時代に 人間がクリエイティブな仕事としてできるのは 農業かな。
無肥料、無農薬の野菜づくりもっとがんばろう!!