【Vue3】自作ライブラリをその場で実行できる多言語対応チュートリアルサイトを作った

:::note
このサイトのお話をします：https://konbraphat51.github.io/HSS_examples/
これがレポジトリです：https://github.com/konbraphat51/HSS_examples
:::

## やりたいこと
### 動機
[初心者向け自作ライブラリを作った！(Qiita記事へ飛ぶ)](https://qiita.com/konbraphat51/items/b138683db352afd77714)  [布教したい！(GitHubレポジトリへ飛ぶ)](https://github.com/konbraphat51/HotSoupScript) **チュートリアルサイトを作ろう！** という経緯です。
ちなみに上記記事が2023/11/14トレンド1位でとても嬉しいです。みなさまありがとうございます。

### どんな自作ライブラリか簡単に
https://qiita.com/konbraphat51/items/b138683db352afd77714　でも十分説明しましたが、HTMLの`canvas`要素に対する描画処理APIを単純化した初心者向けゲームプログラミングJavaScriptライブラリです。
すなわち、ブラウザで動作することを想定しております。

### 要件定義
まず、前提として
1. 布教
2. プログラミング初心者の手引き
3. 多言語対応

をしたいというお気持ちがあります。

1. 自分のライブラリを布教するには、 **どのようなコードで何が実現できるのか** ただちに提示できると訴求性がある。

2. そして、プログラミング初心者を手引きするには、 **コードがコピペでき、コメントによる説明が豊富** である必要があります。

3. ついでに、名だたる列強国と戦うためには、**言語切替** の機能をつけたいと申します。

1と2からは直ちに **「ちゃんと説明されたコードとその実行結果を同時に見せる」** という結論が得られますが、3により **コードコメンテーションも対応言語に変化させる必要** があります。
言語ごとにjsファイルを書く？嫌だなあ。 **綺麗に、変更対応がやりやすいように** まとめます。

## 作りました
### 紹介
作りました。
https://konbraphat51.github.io/HSS_examples/

項目はまだ未完なので、「動く」→「キー入力」のコネクションが飛び過ぎ(2023/11/14現在)なのはご容赦ください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/473e5161-98b5-651f-0865-81901596b5f1.png)

まず簡単な紹介。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/749d1311-a32f-4876-e223-b2aad486268f.png)

ちゃんと言語切り替えできる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/3ac930e5-f20f-d5cc-6edc-3a9cdbbea298.png)

次に項目を選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/c2f1e3bf-7c11-da4d-d473-7709a1969860.png)

言語切り替えできる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/93713c14-0ba2-58bf-c8cf-fdb8a1235eea.png)


選ぶと、自動スクロールで対応するコードまで遷移。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/c51c1863-02c3-1313-e544-1cf4419faca8.png)


コードも言語切り替えできる！！！

![sasa_20231114_214716.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/3aa17f59-475d-016c-5ad2-1afdd982e6c1.gif)


再生ボタンを押すと、対応した処理がcanvasになされる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/25c75054-bda6-3cb0-ba07-287b05e42d9c.png)

フッター。

### 実装
#### CDNでお届けしております
`npm_modules`アンチなので、必要なもの全てをインストールしておく`Vue CLI`ではなく、ページを開いた際に、その都度どこか別のサーバーから必要物をダウンロードしてくる`Vue CDN`で実装。

[CDN Vueで、単一ファイルコンポーネント構成を使い、他言語対応する方法の記事はこちら](https://qiita.com/konbraphat51/items/9cdcc1ceacb3c736c9be)

#### 構成
フォルダー構成はこんな感じ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2602537/6700d9b3-57a2-f59d-01cd-87420caa3320.png)

`src`フォルダに本アプリに必要な物品を。
`src`は"Source Cord"の略なので、原義通りにきっちりソースコードのみを入れる人もいれば、画像でもなんでも必要なデータすべて入れ込む人もいますよね。
[`Vue CLI`では後者が主流](https://qiita.com/4npei/items/364c1f66cdbf5f187c61)なので、同じように。

#### index.html
GitHub Pagesに対応させるため、ルートディレクトリに配置。
[先ほど紹介した記事](https://qiita.com/konbraphat51/items/9cdcc1ceacb3c736c9be)のテンプレートとほぼほぼ同じで、`app`の`div`タグに、`App`コンポーネントをマウントさせています。
（例の自作ライブラリを呼び出しているところだけ追加しています）

#### Examplesフォルダ
Examplesフォルダには、各項目のフォルダが入っていて、その中に
* script.js: 例示するコード
* description.json: 紹介文など
が入っています。

##### description.json
各言語の説明情報をjsonで記述。これは`i18n`の記述と同じですね。
```json: description.json
{
    "en": {
        "title": "Movement",
        "option_description": "Give your character a movement"
    },
    "ja": {
        "title": "動く",
        "option_description": "文字を動かそう！"
    }
}
```

#### script.js
初心者を手引きする感じのコメントとともに、動作させるコードを書きます。
```javascript:script.js
async function main() {
    //en: This is the combination of "Infinite loop" and "Drawing a rectangle"
    //en: Try think how you can move a letter without leave a trace
    //en: It will be very satisfying if you come up with a solution
    //ja: 「無限ループ」と「四角形を描く」のコラボレーションです
    //ja: 先に、跡を残さずに文字を動かす方法を考えてみてください
    //ja: 自分で分かったら気持ちいいですよ

    //en: make variable + set initial position here
    //ja: 変数を作り、ついでに初期位置を設定
    var x = 100;
    var y = 100;

    //en: set speed here
    //ja: スピードを設定
    var speed = 5;

    //en: Adjust character size here
    //ja: 文字の大きさをここで調整する
    SetFont("20px Arial")

    while (true) {
        //en: clear all drawings of the previous frame
        //ja: 前のフレームの描画を消す
        SetColor("white")
        DrawRect(0,0,GetCanvasSize()[0],GetCanvasSize()[1])


        //en: move by key input
        //ja: キー入力によって移動
        SetColor("black")
        DrawText("a", x, y)

        //en: move
        //en: Because the `x` is altered, the position moves
        //ja: 移動する
        //ja: `x`が変化するので、位置が移動する
        x += speed;

        //en: cool time
        //ja: 冷却時間
        await Sleep(10);
    }
}
```

実行させるコンポーネントがこの`main()`を呼び起こす、という感じです。
また、コメントに言語コード+`:`をつけ、対応した言語のコメントを記入。
表示時に、対応言語のみ残っていますね。こうすることで、言語ごとに`.js`ファイルを作らず、まとめて多言語対応コメントをつけることができました。

#### `.vue`ファイル
##### 基本記述
最初の説明カードのコンポーネントの例です。
```vue: Description.vue
<template>
    <div class="warning">
        <p>
            <img src="src\images\description\mark_exclamation.png" 
                width="15" 
                height="15">
            {{ $t("description.warning") }}
        </p>
    </div>

    <div id="description">
        <h2>{{ $t("description.what_is") }}</h2>
        <p> {{ $t("description.what_is_description") }} </p>
    </div>
</template>

<script>
    export default Vue.defineComponent({
        name: 'Description',
        setup() {
            //set up i18n
            const { t } = VueI18n.useI18n()
            return { t }
        }
    })
</script>

<i18n>
    {
        "en": {
            "description": {
                "what_is": "What is HotSoupScript?",
                "what_is_description": "This library enables programming beginers to make an artistic (especially game) programming easily.",
                "warning": "If the \"Hello World\" script doesn't run (canvas shows nothing), refresh the page."
            }
        },
        "ja": {
            "description": {
                "what_is": "HotSoupScriptとは？",
                "what_is_description": "このライブラリは、プログラミング初心者が簡単にグラフィカルな（特にゲーム）プログラミングを行えるようにするものです。",
                "warning": "もし「Hello World」スクリプトが動かない（キャンバスに何も表示されない）場合は、ページをリロードしてください。"
            }
        }
    }
</i18n>

<style>
    #description {
        margin: 10px 20px 10px 20px;
        background-color: #faffdb;
        border-radius: 10px;
        padding: 10px;
        border: 1px solid #000000;
    }

    .warning {
        margin: 10px 20px 10px 20px;
        background-color: #ffdbdb;
        border-radius: 10px;
        padding: 10px;
        border: 1px solid #000000;
    }
</style>
```

まあ基本的な`Vue`の単一ファイルコンポーネントですよね。
一番単純なものを選んでいるわけですが。

##### 項目リスト
一番根本のコンポーネントである`App`に、存在する項目IDをメタ書き（別JSONに書き出してもよかったけど、`App.vue`に書き出すのとあまり違いが分からなかったため）
```javascript:App.vue
        data() {
            return {
                examples: [
                    "HelloWorld",
                    "ChangeColor",
                    "ChangeFont",
                    "Calculation",
                    "Variables",
                    "Sleep",
                    "For",
                    "WhileTrue",
                    "Rect",
                    "Movement",
                    "MoveByKey",
                    "BulletGame"
                ],
                selected_example: "HelloWorld",
                example_descriptions: {}
            }
        },
```

##### Examplesデータの取得
`Examples`フォルダの各項目の説明文を取得するには`fetch`を利用。これも根本の`App`にやらせています。神コンポーネント臭くなりましたね。
```javascript: App.vue
            getExampleData() {
                //read description files
                for (let example of this.examples) {
                    const path = "src/Examples/" + example + "/description.json"
                    fetch(path)
                        .then(response => response.json())
                        .then(data => {
                            this.example_descriptions[example] = data
                        })
                }
            },
```

子コンポーネントにデータを配布するわけですが、`App`の時点で選択言語のデータのみ抽出。`computed`に指定することで、言語切り替え時に自動で更新されるわけです。
```javascript: App.vue
        computed: {
            example_descriptions_locale() {
                let locale = i18n.global.locale.value
                let descriptions = {}
                for (let example in this.example_descriptions) {
                    descriptions[example] = this.example_descriptions[example][locale]
                }
                return descriptions
            }
        }
```

##### スクリプトの表示
先ほどお見せした`javascript`ファイルを、対応言語のみ、`en:`などのシンボルを消して表示させます
```javascript: ScriptViewer.vue
if (line.match("//")) {
    //commentation included in the line

    let another_language = false
    let line_splited = line.split("//")
    let line_edited = line_splited[0]
    for (let cnt = 1; cnt < line_splited.length; cnt++) {
        language_symbols.forEach(symbol=>{
            if (line_splited[cnt].match(symbol)) {
                if (i18n.global.locale.value == language_symbols_dictio[symbol]) {
                    //same language
                    line_splited[cnt] = line_splited[cnt].replace(symbol, "")
                } else {
                    //another language
                    another_language = true
                }
            }
        })

        line_edited += "//" + line_splited[cnt]
    }
    //続く
```

##### スクリプトを走らせる
まずは、読み込みです。

実は、実行に当たって一点ソースコードを改変する必要があります。というのは、サブルーチンを名指しで止める手段がないので、停止ボタンで停止できるように、停止フラグの受信機を作る必要があります。

そこで、自作ライブラリに`while(true)`をするときは`Sleep()`を使うというお約束がありますが、`Sleep()`の直前に受信機を作ります。
```javascript: ScriptLoader.vue
computed: {
    script_edited(){
        let output = this.script

        //define stop flag
        output = "var __stop = false\n" + output

        let returner = "if (__stop) { return }\n"

        //set return under all await() functions
        let lines = output.split(/\r\n|\r|\n/)
        let cnt = 0

        while (cnt < lines.length) {
            if (lines[cnt].match("Sleep")) {
                //await() function
                //set return under the await() function
                lines.splice(cnt + 1, 0, returner)
                cnt += 2
            } else {
                //not await() function
                cnt += 1
            }
        }
        output = lines.join("\n")

        return output
    }
```

そして再生。
再生ボタンのコンポーネントに実行処理を背負わせています。
ゲームプログラミングだと、こんな辺境のUIが重大な処理を背負っていると拡張性の観点から怒られますが、Webフロントエンドのコンポーネント指向の場合はどうなんでしょうね？多分ダメだと思う。

```javascript: StartButton.vue
//start

//stop flag
if (typeof(__stop) == "boolean") {
    __stop = false
}

//reset HSS condition
SetColor("black")
SetFont("10px sans-serif")

//走らせる
StartAsync(main)
```

## 使用感調査
内容はまだ制作中なので置いておいて。

とりあえず個別でプログラミングを教えている方に、ちょっとやらせてみれば「やりやすーい！」とのこと。よかったです（n=1)

## 最後に
いいね頂ければ泣きながら喜びます。（`Vue`記事はあまり見向きされない傾向があります泣）