📚

Tawriという、Web小説向けのエディタを作った。

2024/09/21に公開

作り始めて1週間ほどが経った

ので、ここまでを少しまとめてアウトプットしてみます。

名前の由来

割と安直で、Tauri + Write = Tawri としました。
ネーミングセンスが終わっている自分にしては、まともなので、気に入っています。

Web小説向けのパーサー(AssemblyScript)

https://jsr.io/@l4ph/web-novel-parser
https://github.com/L4Ph/web-novel-parser

Webで動くエディタ。(SvelteKit SSG)

https://tawri.l4ph.moe
https://github.com/L4Ph/Tawri

ローカルで動くネイティブアプリ(Tauri)

https://github.com/L4Ph/Tawri/tree/main/src-tauri

なぜ作ったのか

特段明確にしたいことがあったわけではないのですが、以前から変わらずWeb小説を読んでいました。
それと同時になにかWASM、TauriあたりでのRustの諸々をやりたいなという気持ちが大きくなっていました。
が、RustやC系が死ぬほど苦手で、躊躇っていました。

ちょうど最近Zennで以下の記事を見かけて、それでWASMやってみようかとなりました。

https://zenn.dev/monicle/articles/a543b8ef919058

で、なにから手をつけるかを考えてみましたが、とくにネタがありません。

"WASM パーサー"とかいろいろ調べてましたが、Markdownとかのネタしかなくて、これからやるかーとなっていました。

最近、夜寝る前にWeb小説を読むのが習慣化していて、そういえばWeb小説のルビってどういう記法なんだろうなーと頭の中に浮かびました。

で、少し調べて、この辺を読んでいました。

https://zenn.dev/gaqwest/articles/6cb54e348b5557

"感想としてはカオスすぎないか、これ" でした。

で、この中から"小説家になろう"の記法だけくらいなら、AssemblyScriptの練習になるかなと思って実装してみました。

書き方としてはtsとほとんど変わらないというか、tsのスーパーセットみたいなものなので、型だけ気を付ければあまり問題はありませんでした。

parserが出来上がった。

https://github.com/L4Ph/web-novel-parser

小説家になろうには、実質的に1種類しかない[1]独自記法なので、あとは実際の小説に表示されているHTML<p id="L1"></p>になっているだけなので、そんなに難しくなかったです。
ルビ側のコードの実体もこれだけです。

/**
 * 
 * @param input なろう記法のルビ
 * @returns なろう記法のルビのHTML
 */

export function parseNarouRuby(input: string): string {
    let output = input;

    let pipeIndex: i32 = output.indexOf('|');
  
    while (pipeIndex !== -1) {
        const rubyStart: i32 = output.indexOf('《', pipeIndex);
        const rubyEnd: i32 = output.indexOf('》', rubyStart);
        
        if (rubyStart !== -1 && rubyEnd !== -1) {
            const baseText = output.substring(pipeIndex + 1, rubyStart);
            const rubyText = output.substring(rubyStart + 1, rubyEnd);
            const rubyHtml = `<ruby>${baseText}<rp>(</rp><rt>${rubyText}</rt><rp>)</rp></ruby>`;
            
            output = output.substring(0, pipeIndex) + rubyHtml + output.substring(rubyEnd + 1);
            
            pipeIndex = output.indexOf('|', pipeIndex + rubyHtml.length);
        } else {
            break;
        }
    }
  
    return output;
}

その後、惰性でカクヨムへの対応をしていました。
しかし、どうやってもテストをPassしない記法があったため、軽く飽きてきました。

そうだ、エディタを作ろう。

作るまでの経緯

AssemblyScriptは、ascinitをすると、プロジェクトが作られます。
その中にはindex.htmlが置かれていて、vercel/serveを使って、すぐにコンパイルしたWASMの動作の確認ができるようになっています。
https://github.com/vercel/serve

テストの他に、動作確認として使用していました。

ここで思いつきます。

エディタも作っちゃえと。

実は、自分も昔(中学生くらいの頃なので、8年ほど前)に、"小説家になろう" で小説を書いていました。(今読み直して見ると若かったなぁと感じます。)
その頃と比べると、UIは少し変わっていますが、エディタはほとんど変わっておらず、めちゃくちゃ使いにくいです。


他のWeb小説プラットフォームを見てみても、やはり似たような感じで、ひと昔前のWebサイトだなぁという感想を受けるエディタでした。
Web小説を読めるのは、作者側があってのものです。
が、作者が書くモチベーションを維持できるような使い勝手ではない。と個人的には感じてしまいました。[2]

VSCodeとか使えばいいじゃん。

ITエンジニアの方々は特にそう感じるでしょう。
自分もそう思っています。

しかし、世の中にはzipファイルの解凍すら困惑する方が普通にいます。
なので、VSCodeがなにかを知るまで行かない方もいるでしょう。
また、そこまでこだわると"小説を書く"というところに行くまでの時間でほとんどが取られてしまいます。

/.vscode/や、ESLintすらない状態のプロジェクトを開いたときのセットアップの面倒さとかは少し似ています。
なので、なんとなくこの気持ちを分かる人は、いるのではないでしょうか。

軽く、技術選定のような"なにか"をする

まずWeb小説のエディタを作るにあたってなにが必要か。
そこから考えました。

  • Web小説というからには、レンダリングされるのはHTML
  • ブラウザ上に作るエディタはお世辞にも使いやすいとは言えない(ファイルの読み書きなど)
  • ただ、ネイティブアプリケーションの開発経験はあまりない(言い訳)

という部分がありました。
とくに最後がネックで、Webが下地にあるパーサーをネイティブアプリケーションにもってきてというのができるのか分かりませんでした。

で、そんな自分でももちろんElectronやTauriは知っていて、FFのRustaceanもTauriでいろいろ実装してました。(みんな怖E)

https://x.com/fkunn1326/status/1835074383444066526

https://zenn.dev/parakeet_tech/articles/tauri-vite-mantine-ios

ここでバカ(私)は、こう考えました。
やってみるか

フロントエンドはどうするのか

普段はNext.js、Reactがメインで、Vueもギリギリ書けるくらいだったので、ReactベースでSSGできるフレームワークを採用しようとしていました。
ただ、面白くないし、脳内では1ページのアプリケーションになっていたので、どうせなら普段使わないフレームワークを使おうかなーと考えました。

結果としては前々から使いたいなと考えていた、SvekteKitを採用しています。
useStateがいらん、Svelte / SvelteKit、神。となっているくらいには書いていて楽しいので、個人的には成功かなと思っています。[3]

https://kit.svelte.dev/

分からない部分はGemini(なりのLLM)に聞けばだいたい答えてくれるので、なんとかなっています。

フロントエンド楽しい(Tauriはどこへ...)

ほとんどをSvelteKitで書いてしまっているので、Tauriを書いていません。

いえ、言い訳ですね。
Rustわからん。
なにも考えずにTauri 2.0でプロジェクトをinitしてしまった自分にも問題はありますが、ドキュメントが少なくて本当に分からない。

あと、bun run tauri devしてリロードするたびにアプリケーションのビルドが走るので、ストレスが凄くて目をそらしていました。

もともとTauriだけで作ろうと思っていたのですが、自分のモチベーションが消えるのもよくないと考えました。
そのため、とりあえずSSGで動いているSvelteKitのコードをCloudflare Pagesにデプロイしています。
"Rustも、Tauriもやりたい"という思いは消えていないので、そのうち対応します。

エディタができた。

デプロイも含めて出来上がりました。
だいぶ省いたので雑ですが、この間にもJSRにパーサーをpublishしたり、
https://jsr.io/@l4ph/web-novel-parser

TipTapを採用したはいいものの、独自のパーサー扱う方法があるのか、ないのか分からなくて頭抱えたり。
CodeMirrorの存在を知らずにMonacoを採用したら異様に動作が重かったり。
<svelte:head>で作ったog:imageが動かなかったり。

いろいろありましたが、とりあえず方向性が決められるくらいには作り込めました。

https://github.com/L4Ph/Tawri

https://tawri.l4ph.moe/

フィードバックが欲しい。

自分は小説を"書く"側の人間ではないです。
ここまで実装したほとんどは、こんなのがあったらいいなーだけでやっています。

何某Discordサーバーで、こんな感じに投稿してみましたが、使ってもらわないことにはフィードバックを得ることができません。
なので、この記事で知ってもらう→フィードバックを得る→それを元に実装をしてみる。
そんな感じでのんびりと進めようと考えています。

他所で得たフィードバックはすべてissueに自分で書く予定です。
GitHubアカウントをお持ちの方は、Starはもちろん、issue、PR等お待ちしています。

パーサー、エディタともにMITライセンスですので、お好きに改変していただいて構いません。

ToDo

正直作り始めてからまだ1週間ほどなので、やりたいことはまだまだあります。
適宜Zennでアウトプットしながら進めていければよいかなと考えています。

  • Tauri版のリリース
  • textlint(Web Worker版)での文章校正
  • Tauri版のショートカット入力対応
  • モバイル版
  • Web小説プラットフォームの対応を増やす。(アルファポリスやハーメルンなど)

etc...

将来的にはwindow.ai()で、ローカルのGemini Nanoを使っていろいろやりたいなとか考えているので、GitHubのStarや、フィードバックをお待ちしています。

最後に

Tauriのファイルの読み書き分かる方、Xでお待ちしています。
Tauri 2.0の情報が少なすぎて詰んでいます。

https://x.com/R0u9h

長くなりましたが、ここまで読んでいただき、ありがとうございました。

脚注
  1. なろうの機能としてはルビと傍点の挿入があるが、レンダリングされるHTMLはルビとして扱われる。 ↩︎

  2. それを批判する気もないですし、慣れればあまり気にしないのは自分もなんとなく分かります。 ↩︎

  3. 歳なのか、useState()でコードの見通しの悪さが辛くなりつつあります。 ↩︎

Discussion