generated at
正規表現の変形で作る独自記法WiKi Parser

shokai @shokai


右のメニューのStart Presentationでスライドになります

応募した内容
感想など




今日の話
1. 独自記法をどんどん作って乱立させよう
2. その実装方法

元々20分ぐらいライブコーディングで説明してもうまくまとまらなかった話を飛び入りで5分にまとめてリベンジしにきた


CosenseというWiKiを作っている
独自のWiKi記法を実装



なぜ独自記法?
ページ間リンクが最も重要な機能だから
1ページ内の装飾よりも
Markdownは記号が予約されすぎていて拡張余地が無い
[ ( も全部予約されてる
じゃあ全部作ろう

Scrapbox記法
[builderscon]
buildersconというリンク記法になる
見た目はhashtagだけど内部的にはリンク記法と同じ

書いたそばからリンク構造を辿って、推薦が更新されていく


記法は重要
ドキュメントツールにはコンセプトに最適化した記法が必要
体験が中途半端になるので、リンク記法を最優先したい
必ずしもMarkdownが最適では無いし、そもそもプログラマしか使ってない
例: <img> <a> を付けたい
Markdown
[![タイトル](画像URL)](リンク先URL)
Scrapbox記法
[画像URL リンク先URL] もしくは
[リンク先URL 画像URL] どっちも可
物を覚えない為にパソコン使ってるんだから順不同で書きたい

互換性とか気にしなくていいのでは?
良いアプリならユーザーが勝手にmarkdownとの変換ツール等を作ってくれる
どんどん分裂していこう


WiKi parserを簡単に作る
正規表現でやる
まじめに1文字ずつ読むようなparserとか書かない
React使うのでclient js側でparseする
処理はclientに分散するので実行速度は無視
サーバーでparseしない
どうやってもReactのrenderよりは軽いので気にしなくていいと思う

基本的な流れ
生テキストがある
今日は[builderscon]に行ったよ

1. 生テキストを正規表現Aでsplitする
記法部分と生テキストに別れる
今日は [builderscon] に行ったよ になる
2. 記法部分から正規表現Bでmatchする
記法の中からパーツが取れる
[builderscon] から builderscon を取り出す
3. あとはtreeを返してReactでDOMにすればいい

つらみ
よく似た正規表現AとB、2つ書く必要がある
1つではどうしてもできない
その2つを同時に修正しないとparserが壊れる
このスライドでは簡単な正規表現で説明しているけど
実際は /\[([^\[\]]+)\]/ とかなので、2つあると間違える

よく似た正規表現
1. 生テキストから記法とそれ以外を分ける
js
> "今日は[builderscon]に行ったよ" .split(/(\[.+\])/) [ '今日は', '[builderscon]', 'に行ったよ' ]
2. 記法の中からパーツを取り出す
js
> "[builderscon]".match(/\[(.+)\]/) [ '[builderscon]', 'builderscon', // パーツが取れた index: 0, input: '[builderscon]' ]
ここで重要なのは丸カッコ ( ) の位置で
1と2で正規表現の中にあったり外にあったりする
これを変換できれば正規表現1つで済むのでは?!


つまり
/\[(.+)\]/
/(\[.+\])/
を変換したい

解決方法
正規表現Cを宣言し、AとB2つの正規表現に変換する
flagsとsourceを使うと変形できるじゃん
js
> var reg = /\[.+\]/mi undefined > reg.flags 'im' > reg.source '\\[.+\\]' > new RegExp('(' + reg.source + ')', reg.flags) /(\[.+\])/im // 前後に丸カッコ付けた新しい正規表現


capture ( ) をどうにかする
/\[(.+)\]/
/(\[.+\])/
を変換したいが、丸カッコを単純に削除してしまうと壊れる正規表現がある
(a|b) とか
丸カッコを無効化すると良さそうだ

(a|b) (?:a|b) にする
?: を付けると、グループ化はするけどmatchには出てこないようになる
/\[(.+)\]/
/\[(?:.+)\]/ にしてから、
/(\[(?:.+)\])/ にするといい

sourceを地道にreplaceしてnon-capturing groupsにした正規表現を返す
js
// disable "capture" in RegExp // replace (~~) with (?:~~) module.exports = function (regexp) { return regexp .source .replace(/\(\((?!\?)/g, function (leftParenthesis) { return leftParenthesis + '?:' }) .replace(/(^|[^\\])\((?!\?)/g, function (leftParenthesis) { return leftParenthesis + '?:' }) }
これで2つの正規表現が変換で作れるようになった

npmにした



最終型
ここから色々やると、1つの正規表現を書くだけでparserが作れるようになる
js
export const parsePageLink = createNodeParser(/\[([^\[\]]+)\]/, ([, page]) => ({ type: 'pageLink', page: title }))

まとめ
正規表現1つ書けばよくなってメンテナンス性が上がった