Tauri + Svelte + Milkdown でマークダウンエディタを作った

# はじめに
マークダウンエディターを作りました。
機能はめちゃくちゃシンプルで
- mdファイルダブルクリックで直接起動出来る
- タブ機能がある
    - 前回起動していたタブを保持する
- 起動が爆速
- WYSIWYGで使える
> WYSIWYG（アクロニム: ウィジウィグ）とは、コンピュータのユーザインタフェースに関する用語で、ディスプレイに現れるものと処理内容（特に印刷結果）が一致するように表現する技術[1]。What You See Is What You Get（見たままが得られる）の頭文字をとったもの

を目標にしました。
（マークダウン版のサクラエディタみたいな感じ）

ちなみに今回使った技術は全部**ズブの素人**なので、的はずれなコメントなどがあるかもしれません。
よろしくお願いします。

# 出来たもの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2603116/56166d98-6c2b-154c-eedd-11fc0fdbd53f.png)

満足！

## 使ったもの
- [Tauri](https://tauri.app/)
- [Svelte](https://svelte.jp/)
- [Milkdown](https://milkdown.dev/)
- [React](https://ja.react.dev/)（最初こちらにしてたけど、諦めてSvelteに切り替えた）

# 引っかかった部分
これだけだとただの学習発表会で終わりなので、作っている中で引っかかった部分を色々と雑に記録しておきます。

## React
### コンポーネント間で変数を共有したい場合の話
Reactは基本的に変数の共有が、親から子へとしか出来ませんでした。
（useStateを親で宣言し、子に渡す形）

また、その変数が更新された場合はstateを保持するコンポーネント自体がまるまる再描画されるため、個人的に使い勝手が悪く・・。
（エディタのカーソル位置が毎回初期化されてしまう！）

正直、これが解決出来なくてReactの採用を諦めました。
ただ後日友人に聞いたところ、**Redux**というライブラリを使うことでグローバルな変数を定義することが出来るようです。

毎回再描画されるのも解決するかはわかりませんが、試してみていただければ・・。

## Tauri
### タイトルバーをオリジナルなものにしたいなら、最初からしといた方が良い
https://qiita.com/takavfx/items/9bf518a5bc3cc7c19509

こちらを参考にすることでさっくり実装ができます。
正直、下手にTauri標準のタイトルバーを使ってRust側でメニューを作りつつ、JavaScript側で受け口を用意して・・
とやるより、こっちのほうが手軽かつ馴染み深かったです。

「難しそうだから一旦標準で作って、後で載せ替えようかな～」という気持ちの場合、最初からこっちにしておくことをおすすめします。

### タスクマネージャーなどで"A Tauri APP"と表示される問題を解決する
基本的には`tauri.conf.json`の`productName`に設定した名前で表示されるのですが、一部のより基幹に近い場所？では **"A Tauri App"** と表示されてしまい困っていました。

解決方法はこちら。

https://github.com/tauri-apps/tauri/issues/1849

`tauri.conf.json`の`tauri > bundle > shortDescription`をセットすることで、こちらで設定した名前で表示されるようになります。
（キャッシュが残るのか、再インストールしても変わらず再起動することで反映されました）

### globalShortcutを使うかどうかはきちんと考えよう
https://tauri.app/v1/api/js/globalshortcut/

こちらのglobalShortcutですが、「global」というだけあってアプリがアクティブじゃないときにも有効になっています。

今回テキストエディタによくある「Ctrl + S」で保存するショートカットを用意したのですが・・。
globalで登録されたので、今回作ったアプリが起動されている間、アプリがアクティブじゃなくてもVSCodeでCtrl+Sが使えなくなりました。

ショートカットキーは他の登録方法も可能です。
適切な方を使いましょう。
- アプリがアクティブじゃないときにも有効にしておきたい。
なおかつ、他のアプリのショートカットが動かなくなっても問題ない。
    - globalShortcutにregisterで登録する。
- アプリがアクティブなときだけ有効にしたい。
    - document親要素のonkeypressあたりにショートカット用の関数を割り当てておく。

## Svelte
### 出来る限りCSSは`.svelte`内の`<style>`に書こう
なんとなく、`<style>`タグを使うのってナンセンスというかクラシカルというかなんというか・・。
みたいな拒否反応起こしません？

それでわざわざ別途`.css`ファイルを作ってやってたのですが、svelteの`<style>`には大事な要素があります。

それは、ここで定義した`<style>`は他のコンポーネントには一切影響しないということです。
なので下記のようなケースでも、それぞれ別のデザインとなります。

<details><summary>サンプルコード</summary>

```svelte:a.svelte
<div class="container">
    ここはa.svelteです。
    背景色は赤色で表示されます。
</div>

<style>
.container {
    background-color: red;   
}
</style>
```

```svelte:b.svelte
<div class="container">
    ここはb.svelteです。
    背景色は青色で表示されます。
</div>

<style>
.container {
    background-color: blue;   
}
</style>
```
</details>

**ただし欠点もあります！**

上記の通り一意性をもたせる処理の兼ね合いか、コンポーネント内に記載のないタグやクラス、Idに対してのcssを書けませんでした。
（例えば子コンポーネント側のstyleを親には書けない）

https://dora.bk.tsukuba.ac.jp/~takeuchi/?%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%2Fsvelte%2F%E8%A8%98%E4%BA%8B%E3%81%AE%E8%A6%8B%E3%81%9F%E7%9B%AE%E3%82%92%E3%81%84%E3%81%98%E3%82%8B

なので、こういうときは`:global`をつけて対応することになります。
（詳しくはリンク先参照）

### popupを作るのがちょっとだけ難しかった
いい感じのpopupライブラリが見つからず、自作しました。

<details><summary>コード</summary>

```svelte:Popover.svelte
<script lang='ts'>
    import { onMount, onDestroy } from "svelte";

    export let title = '';
    export let triggeredBy = '';

    let popoverElement:HTMLDivElement;
    let visibility = 'hidden';
    let left = 0;

    const onMouseOverHandler = () => {
        visibility = 'visible';

        // 配置するポジションを計算
        left = - ((popoverElement.clientWidth / 2) + 20);
    }
    const onMouseOutHandler = () => {
        visibility = 'hidden';
    }

    onMount(() => {
        const triggerElement = document.querySelector(`#${triggeredBy}`);
        triggerElement?.addEventListener('mouseover', onMouseOverHandler);
        triggerElement?.addEventListener('mouseout', onMouseOutHandler);
    });

    onDestroy(() => {
        const triggerElement = document.querySelector(`#${triggeredBy}`);
        triggerElement?.removeEventListener('mouseover', onMouseOverHandler);
        triggerElement?.removeEventListener('mouseout', onMouseOutHandler);
    })
</script>

<div class='pos_relative' style='--visibility:{visibility};'>
    <div class='pos_absolute popover_container' bind:this={popoverElement} style='left:{left}px'>
        <div class='title'>
            {title}
        </div>
        <div class='content'>
            <slot></slot>
        </div>
    </div>
</div>
<style>

    .title {
        font-weight: bold;
        padding-bottom: 5px;
        margin-bottom: 5px;
        border-bottom: 1px solid var(--color-gray-4);
    }

    .pos_relative {
        position: relative;
        visibility: var(--visibility);
    }

    .popover_container {
        position: absolute;
        top: 45px;

        width: max-content;
        max-width: 70vw;
        padding: .7rem;
        border-radius: 5px;

        background-color: var(--color-milk);
        color: var(--color-gray-2);
        opacity: 0.85;
    }

    .popover_container :global(p) {
        margin: 2px 0;
    }
</style>
```

</details>

このうち苦戦したのは下記です。
- ホバーした要素を中央として表示するのが難しかった。
    - `{#if}`で表示切替をすると、`bind:this`が取得出来ない。
→ 表示切替をvisiblityで行うことで要素の取得が可能となった。
- 親要素からDOMを渡してもらって表示する方法がわからなかった。
    - `<slot>`を使うことで埋め込める。

### `.ts`ファイルでstoreを使うのが難しい
`.svelte`であれば`$`を使うことで簡単にアクセス出来るのですが、`.ts`ファイルだと使えません。

どうするかと言うと、シンプルに`update`などを使いましょう。

## Milkdown
### pluginの使い方がよくわからない・・。
公式が提供しているpluginに関しては、実は公式ページに簡単な説明が乗っています。

https://milkdown.dev/docs/api/plugin-listener

開いて見様見真似でやれば、最低限はいける！

### `.make()`が思うように動かない
あまりしっかりと理解出来ていないのが悪いのですが・・。
一度Editorを作った後、初期化をする方法がわかりませんでした。

ということで、それぞれ下記の問題の対応方法メモ。
- タブごとに異なる値を初期値として持たせたい
    - タブごとにEditorを格納するdivを用意し、それぞれに作成させる
- 値をがっつり上書きしたい
    - https://milkdown.dev/docs/api/utils#replace-all-markdown-string-flush-boolean-false-fn-ctx-ctx
↑これを使う

### 編集履歴が他タブと共有してしまっている
もともとタブ切り替え時の処理を`replaceAll`でやってたのですが、そうすると編集履歴は脈々と続いていくので、タブを切り替えた後がやっかいなことになります。

解決方法としては、「**タブごとに異なるEditorを用意する**」です。

### チェックボックスが使えないよ！
公式HPのPlayGroundには表示されているチェックボックスが何故か使えない・・。
と悩んでいたのですが、どうやらチェックボックスは「2パネル表示」のときしか使えないらしいです。
（どうやって2パネル表示にするかは知らない・・）

# おわりに
楽しかった！
自作の一番良いところは、ちょっと気になる動きやデザインがある時に自分ですぐ直せるところですね。