Pluto.jl ノートブックの上で JavaScript/CSS を使ってカスタムウィジェットを作ったよ

# 本日は

Qiita のダークモードは良いですね．最近は Pluto.jl を使った Julia のノートブック環境でJulia パッケージのチュートリアル作成・整備を行なっています．人の欲望は無限大なので Pluto.jl でいろんなことをしたいし不足部分は自分の裁量でカスタムしたいことも出てきています．

## 最近作ったもの

### PlutoMonacoEditor.jl

Julia 以外のコードの編集を快適にするためのツールです．エディタは Monaco Editor を使っていますので VS Code を使った編集体験に慣れている方であれば作ったパッケージは役に立つのかなと思っています．以下の図は Rust の簡単なコードを編集する画面をセットアップし，エディタで入力されたコードを `main.rs` に一時ディレクトリに保存，それを裏で `rustc main.rs` を叩いで実行するデモを表しています．単にエディタを提供するだけでなく，編集したコードを Julia 側に対して `String` 型の値として渡すことができます：

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/885ff7a5-353c-c3f6-d6cf-efb50333bd2d.png)

https://github.com/AtelierArith/PlutoMonacoEditor.jl

Monaco Editor については下記の記事で解説がなされています．ブラウザで表示させるやり方も参考にしました．

https://qiita.com/kyok01_japan/items/41c7ab287b2233bd4313

### PlutoEditorColorThemes.jl

もう一つ紹介します．こちらは Pluto Notebook の上で入力するセルをカスタマイズできるパッケージです．デフォルトでは下記のように Monokai 風になります．私はリッチなテキストエディタは Sublime Text から入門した人なので Monokai のようなカラフルなのが好きです．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/ae4c8bdd-96de-ce77-53af-cd8606b36287.png)

https://github.com/AtelierArith/PlutoEditorColorThemes.jl

`setcolortheme!` 関数を実行することで Pluto.jl が持っているコードのシンタックスハイライトを担う CSS を上書きさせています．

## PlutoUI.jl について

PlutoUI.jl はノートブックユーザがスライダー，チェックボックス，ドロップダウンなどのUIを構成する部品(以下ではウィジェットと呼ぶ)を提供します．

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">できたでキュ！ <a href="https://t.co/QrdpdwNlYl">https://t.co/QrdpdwNlYl</a> <a href="https://t.co/wH9WCIL1p7">pic.twitter.com/wH9WCIL1p7</a></p>&mdash; ごまふあざらし(GomahuAzarashi) (@MathSorcerer) <a href="https://twitter.com/MathSorcerer/status/1844657873806340505?ref_src=twsrc%5Etfw">October 11, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

上記の例はスライダーが一個しかないですが，

https://plutojl.org/en/docs/advanced-widgets/ 

にて紹介されているように複数のウィジェットを組み合わせて作ることもできます．ササッと対話環境を作る分には PlutoUI.jl で十分ワークします．

他にも下記のような Julia パッケージがあり色々面白い UI をユーザに提供することができます．

https://github.com/hellemo/ShortCodes.jl 

https://github.com/JuliaPluto/PlutoTeachingTools.jl 

### `@bind` による Julia との連携

Pluto ノートブックの上でウィジェットの値と Julia の変数を結びつけるためにはセルに下記のような Julia のコードを挿入することになります．

```julia
@bind <Juliaの変数> <HTMLを返却・表示する関数呼び出し>
```

例えば下記の例は -4 から 4 の範囲を 0.1 刻みで動くスライダーを定義します．

```julia
# 事前に using PlutoUI を実行する
@bind x Slider(-4:0.1:4, defaut=2)
```

このようにすると初回の評価では `x` は `default` で指定した 2 が格納されます．ノートブックユーザがマウスでグリグリ動かすと `x` の値はそれによって動きます．それによって `x` に依存している別のセルも自動的に実行・反映されます．`Slider` の実装は下記のリンクに対応します．

https://github.com/JuliaPluto/PlutoUI.jl/blob/225cef18b4c9da13a9020a8861b2ab90732263fa/src/Builtins.jl#L127-L228


## 以下は物足りない人・オレオレ実装を作りたい人向け

### Web 開発者カモン！

JavaScript によるウィジェットを作る能力を持つ読者が Julia に参入する余地は十分にあります．

開発者は `h1`, `div`, `span`, `input` のようなHTMLを含むソースコードを返す「関数を実装する」または・「表示する仕組みを持つ構造体」用意すればOKです．関数の実装は極端にシンプルな例は下記のような Julia の関数を実装することを指しています．

```julia
begin
    function myh1()
        return html"<h1>Hi myh1 function</h1>"
   end

   myh1()
end
```

`html` を文字リテラルの前につけることで HTML オブジェクトを作ります．

```julia
julia> html"Hi" |> typeof
HTML{String}

julia> (@html_str "Hi") |> typeof
HTML{String}
```

後者の表示する仕組みを作るというのは自作の構造体（`MyType` に対応）を用意し `Base.show(io::IO, m::MIME"text/html", mytype::MyType)` を実装することに対応します．

```julia
begin
    using HypertextLiteral: @htl
    struct MyType
        msg::String
    end
    
    function Base.show(io::IO, m::MIME"text/html", mytype::MyType)
        show(io, m, @htl "<h1>$(mytype.msg)</h1>")
    end

    mt = MyType("Hi, MyType")
end
```

実行した結果は次のとおりです．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/a2bb9b2b-74c9-51dc-9a43-c4a0f611aac9.png)


ここで `@htl` マクロは `"<h1>$(mytype.msg)</h1>"` を適切に文字列補間するために用いています．この部分を仮に `html"<h1>$(mytype.msg)</h1>"` にするとドルマークを含めた `$(mytype.msg)` という文字列が大きいフォントで表示されます．`@htl` はこの問題を回避するために用いられます．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/b5646925-681a-2db6-7a48-be014f5836bd.png)

プラクティカルなパッケージの開発だと `@htl` マクロを使うことが多いです．PlutoUI.jl の実装を眺めてみるとわかります．

## Julia と連携したい

`@bind` マクロを使うんでしたよね？

```julia
@bind x MyType("Hi, MyType")
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/696245fc-b2d5-b6df-44f3-80d90a60e08d.png)

連携してないじゃねーか！はい．．．もう少し実装を詳しく書く必要があります．`x` とHTML
を構成するどの要素の値と紐付けしなければいけないかを開発者は指定する必要があります．

```julia
begin
    struct MyType2
    end
    
    function Base.show(io::IO, m::MIME"text/html", mytype::MyType2)
		ret = @htl """
		<span>
			<script>
			  	const span = currentScript.parentElement;
			  	span.value = "AAA";
			</script>
		</span>
		"""
        show(io, m, ret)
    end
end
```

実行例は下記のようになります．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135978/b4f24431-8e8e-788a-80d3-1328bfbb12fd.png)

## もう少しプラクティカルな例

CounterButton の実装がわかりやすいと思います．

https://github.com/JuliaPluto/PlutoUI.jl/blob/225cef18b4c9da13a9020a8861b2ab90732263fa/src/Builtins.jl#L376-L393

上記の実装から HTML の要素を抜き出し整形をかけると次のようになります．`<span>...</span>` という構造を持つHTMLを表示させていることがわかりますね．

```html
<span>
  <input type="button" value="$(button.label)">
  <script>
    let count = 0
  	const span = currentScript.parentElement
  	const button = span.firstElementChild
  	
  	span.value = count
  	
  	button.addEventListener("click", (e) => {
      	count += 1
      	span.value = count
      	span.dispatchEvent(new CustomEvent("input"))
      	e.stopPropagation()
  	})
  </script>
</span>
```

`currentScript` で上記の `<script>` 要素を指し `parentElement` で `<script>` 要素の親 `<span>...</span>` としている `span` を指します．`dispatchEvent`, `addEventListener` を用いてイベントハンドラの設定をしています．`span.value = count` にあるように `value` に入っている値と `@bind x <...>` で指定した `x` が紐づきます．

ややごちゃごちゃしてます（自分がかなり誤魔化して書いてるのもありますが）が，なんとなく実装の方針は伝わったかなと思います．

ちょっと Julia に興味がある Web 開発者の参入お待ちしてます．