LoginSignup
0
0

tailwindcss で prefix に tw- をつけた時の話 (Github Copilot)

Last updated at Posted at 2024-07-02

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 に 「私は ホームページは得意です。いくつかの質問に回答するだけで素敵なページを作成できます。」と言い出す時が来るのかな。
そんな時代に 人間がクリエイティブな仕事としてできるのは 農業かな。
無肥料、無農薬の野菜づくりもっとがんばろう!!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0