tailwindcss で prefix に tw- をつけた時の話 （Github Copilot）

## tailwindcss と bootstrap の共存のために

bootstrap 5 で 作成したページの中に tailwindcss で作成した
部品を使いたくなって

tailwindcss には `tailwind.config.js` に
``` js
module.exports = {
  prefix: 'tw-',
```
という定義を書くと
``` html
<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` ファイルに

``` C#
// 入力ファイル
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
```

``` C# 
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 の説明部分](https://tailwindcss.com/docs/configuration#prefix)

を確認したら
``` html
<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 をつける処理を
    組み合わせたコードを以下に示します。
``` C#
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 を加えてください。
```

その回答が以下

``` C#
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 文字列を付け加える処理を見直します。 
１）tw を付加する関数 文字列の先頭が - （マイナス）で始まる場合は -tw をつける。
 それ以外は tw- をつける。 
２）class の文字列に : が含まれている場合は : の前後で文字列を分割して
 後ろ側の文字に １） の 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` の回答が以下

```C# 
// 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` 関数の 
``` C#
return "-tw" + className.Substring(1);
```
は
``` C#
return "-tw" + className;
```
ですね。

最後に 
```
inDir 配下にある html ファイルを HtmlReplace 関数を呼び出して 
HTML の中身を置き換えて ファイル名に _fix をつけて 
同じディレクトリ保存するコードを書いてください。
```
とお願いして 生成されたコードが 下記。
``` C#
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 に変換している。

今までに作成した関数をすべてマージして出来上がった最終コードが下記。
``` C#
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- を付加するようなコードに変更してください。
```

その回答が以下
``` C#
// 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` を使って対話的にコード生成した時の例を書きました。

作業内容をタスクに分類してみましたが、★１，★３、★４ の部分は プログラム作成の 急所であり、まだ 生成AI ででは代用できないようです。 

コードでやりたい事と、その具体的な方法の概要が見えている。（★１）
おおざっぱに手順を指示すると それを実現するプログラムコードを生成してくれる（★２）
そのコードをテストする（★３）
予想と違う動きをしたので調査（★４）
コード生成の条件を変更を指示する（★５）
生成AI が 新しい指示でコードを生成する（★６）
生成したコードを動かして再度、動作確認（★７）
作成したコードを今回限りで捨てるか、再利用するのか、
誰でも使えるように体裁を整えるのか、今後のメンテナンス方針を検討する。（★８ 今回は WEBに公開して完了。自分も将来もしかすると使う事があるかも。）
すべての仕様を網羅しているか、いろいろなバリエーションで正しく動作するのか？
ちゃんときっちり仕様と照らし合わせてバリエーションテストを行う。（★９ 未実施。WEB に公開する事で誰かが不具合を発見して報告してくれる事を期待）

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