タイトルの通り。
Stay Homeで暇だし、小説でも書こうかなと思って書き始めたんですが、小説を書くツールが定まらなすぎて小説は書かないで小説書くためのマークアップ言語を考えてpegjs使ってパーサー作ってました。(これを展開してブログ記事にするのが面倒くさい) なお小説は結局現時点でひとつも書いてない模様……
— だから僕は生活をやめた (@515hikaru) 2020年5月17日
Stay Home だし暇だからなんか創作するかと思って小説を書こうと思った*1。だけど小説を書くツールで気に入るものがない。自分でサイトを持つとか面倒だしユーザーが来てくれる気がしないから Pixiv とかカクヨムとかがいいなと思ってとりあえず既にアカウントがあった Pixiv 小説をみたんだけど改行が改行と判定されるので1段落を書くのにすごく横に長い文章を書かないといけなかったりする。そういうのは我慢するにしても、よい書き方というか、良いスタイルみたいなのをググると全角スペースを行頭に空けるべきだとかいろいろなお作法が出てくる。
最初は「そんなん sed
でえいやってやればいいやん」みたいに思ってたので、実際 Python を書いてみたんだけどこれがちょっと複雑なことになるとダメで手動でなおす羽目になる。手動で直す余地があると CI の artifacts をコピーして Pixiv 小説にバンと貼って投稿! みたいなソリューションにならなくて困ったなぁということで、パーサーを書いてみることにした。
うん、先に言っておくけど、今後小説を書く気が微塵もない人の話が続く。もっぱら PEG.js というパーサージェネレータの話と、 Node.js のコマンドラインツールを作る話だ。プログラミングを知らない人にはちんぷんかんぷんだと思う。もちろん小説は一切書いてないわけではなくて、「ツールの動作確認のために」、少しは書いたんだけど、とても人様に読ませるようなものにはなってない。
あと、動くものにはなったけど、現時点ではプログラミングにゆかりがない人に使えるような状態にはなっていない。ていうかぶっちゃけ Google Docs でいい。小説を書きたい人がもしこの記事を読もうとしていたらとりあえず Google Docs を開いて書き始めろ。俺が言えたことじゃないけど。
まだ計画してないけど、気が向いたらこのツールがブラウザで動いたら面白いなとか考えている。でも気が向くかはわからない。
何を作ったか
まず、自分は Pixiv 小説のエディタしかみていないので、基本的にこの記事は Pixiv 小説のエディタというか投稿時のフォーマットを念頭において書く。とはいえ、別に日本語の小説を書くルールは変わらないから Pixiv 小説に限った話でもないと思う。
個人的に Pixiv 小説について気になったのは下記の点。
- 改行が改行と認識される
- 1段落が長くなると、横に長い文章を書くことになりテキストエディタで扱いづらいとか、Git の diff が読みにくいとかの問題が起こる
[chapter:第一章]
みたいなオリジナルのトークンが(数は少ないけど)ある。覚えられない。- 行頭の全角空白や「!」のあとの全角空白が完全手動
技術書を書いたときに RE:VIEW + LaTeX で書いた身としては人力で気をつけないといけないことが多すぎていやになってしまうわけで*2。とはいえあくまでほしいのは Pixiv 小説に貼るためのプレインテキストであって、PDF ではないから LaTeX を使いましょうというのも解決にならない。Booth で売るなら LaTeX でもいいんだけど、そんなお金もらうような作品を書きたいと思っているわけじゃないし。
んで、作った。Pixiv 小説を書くための言語として考えたから pnovel なんだけど、別に Pixiv 小説以外にも使えるので、ミスリーディングな名前だったかもしれない。まぁ少なくとも現時点では Pixiv 小説以外は想定していない。
ざっくり次のような内容になっている
#
から始まると章立て。# foo
は[chapter:foo]
と変換- 「と(で始まる文章は行頭に全角空白を入れない
- 文章の途中で改行を挟んでも改行されない。改行したい場合は空白行を1つ挟む
- レンダリング結果において空白行を挿入したい場合は
[newline]
トークンを書く
まぁ要するに、
# 見出し 普通の文章。 改行をしても 改行されません。 改行したいときは 1行空けます。 「会話文」 一行空けたいときは [newline] とかく。
が、
[chapter:見出し] 普通の文章。 改行をしても改行されません。 改行したいときは 1行空けます。 「会話文」 一行空けたいときは とかく。
と変換される。まだ「!」だとか「?」だとかの対応はできてないんだけどおいおいやる。
どう作ったか
PEG.js
まず自分でパーサーを作るのは初めてだったんだけど、『Go言語で作るインタプリタ』を読んでいたおかげで「普通パーサーを作るときはパーサージェネレータを使う」という知識はあった。ただ名前は yacc とかそういうのしか知らないし、なんか難しそうだと思っていた。今回やりたいことはプログラミング言語処理系を作ることではなく、どちらかというと正規表現の置換に近いものがあるので、それでなんかないんかなと探したら PEG にたどり着いた*3。ブラウザで試せるってことと、似た事例を見つけたので PEG.js を採用した。
- ブラウザでデモを動かしてみる
- なんもわからん
- いろいろと
console.log(obj)
とやりまくってみる - なんかだんだんわかってくる
- 完全に理解した
- 自力で構文を定義してみる
- 実行時エラーで死ぬ
- なんもわからん
これをひたすら繰り返し、なんとなく理解していった。だんだんわかってくると、kmizu さんの PEG基礎文法最速マスター - kmizuの日記 に書かれている日本語とパーサーがどう動くかの対応が取れてきてやっと自分の頭で考えられるようになってきた気がする。
Node.js でコマンドラインツール作り
パーサーには JSON を吐いてもらうことにして、JSON を走査するコードは自分で書くことにした。パーサーの部分のコードと評価(文字列として組み立てなおす)部分のコードを別にしたかったのと、同じ構文から複数のフォーマットに変換できたほうがいいんじゃないかなと思ったから。だけど、そんな需要はないかもしれない。
import
ってやると node
のエントリーポイントにできないってことも知らないくらいの JavaScript 初心者の僕だったわけですが、 webpack
を使ってエントリーポイント作りをし(雰囲気で webpack.config.js
を書いた。人生で初めて)、テストもどうテストすりゃいいんだと Twitter で呟いて教えてもらった jest を使い*4、ESLint にスタイルはおまかせして GitHub Actions に CI させてたらなんかいつの間にかいっぱいコミットしてた。ブラウザに使えるものではないけど、初めてこの辺の JavaScript ツールチェインを自分で触ったのは普通に勉強になったかも。TypeScript でなぜ書かなかったのか? 全く理由はないです。。
Docker イメージ作り
上の方にちらっと書いたけど成果物を CI で回収してコピペして投稿ってしたいので、CI で使いやすいように Docker Image を作りました。
特に GitLab CI や CircleCI だと自分の Docker イメージをひょいっと持ってきて自作ツール動かすとかができると便利かなと。他にもローカルでさっと動かすという用途にも一応使えます。
docker run --rm -v $(pwd):/work 515hikaru/pnovel:0.3.1 pnovel /work/main.pnovel > main.txt
やってないこと
パーサー面
- 全角英数/半角英数のどちらを使うかの統一
- 他のユーザーみていると全部全角という人が多い気がする
- 自分は半角でうつ習慣があるので、自分が全角をうつのでなくパーサーに全角にしてほしいので対応するかも
- 上にも書いたけど「!」や「?」を使ったときの全角空白
ツールとして
npm publish
してない- アカウント持ってないだけ
- README とかも真面目に書いてない
- あと俺以外誰も使わんやろと思ってる
- そのうちやるかもしれない
- 複数ファイル、標準入力対応はやりたい
ツールとしてどうこうというよりも、パーサーとしてどうこうというほうが気になるところが多い現状。正直コードは思いついたまま書いてそんなに見直してないし(そもそも分量もないけどね)。
終わりに
なんか久々にガッツリコミットして作ったけど、そもそも小説を一本も書いていないのでまじでなにをしているんだろうという気持ちに今更なっている。仕事忙しいし、しばらくは書けないんじゃないかなぁ。
マジでなんで作ったのか。あとブログにする前にせめて README くらい書いたほうがよかった気がする。まぁいっか気が向いたら随時更新します。
*1:ちなみに絵の練習を始めたりもしたけどこっちはペンタブを買って満足している。
*2:textlint で日本語小説用の Lint 設定ができるのはみたんだけど、直すのは手動だし、原稿に機械的な全角スペースを入れたくないなと。
*3:Python の CSON パーサーを探してたときに、まんま cson というパッケージがあったんだけど、それの中身はどうも PEG に準拠したパーサージェネレータを使って CSON パーサー作っているっぽい。というのがこのパーサーづくりでやっとわかった。
*4:パーサーはまじですぐぶっ壊れるのでテストがないと死ぬ。