textlint editor - ブラウザでも動くPrivacy Firstの文章校正ツールを作る話
自己紹介

テーマ
- textlint
- Privacy Firstな校正ツールを作る
- サーバにデータを送らずに、ローカルで文章のチェックをする
textlintとは?
- textlintJavaScriptで書かれた文章のLintツール
- Markdown、Re:View、HTMLなど文章構造をパースしてからチェックする
- 一般的なスペルチェッカーは構造を見ないので誤検知する
- 200弱ぐらいのルールがある

textlintのユースケース
文章に対するCI
- 文章に対するContinuous Integration(CI)を持ち込む
- そのためどちらかというと技術的な用途がメイン
Easy的な使い方
目的

grammarlyの問題
- textareaに入力した文字が全てサーバに送られる仕組み

類似するツール
サーバに入力文字を送ることで起きる問題
DeepLの学習

Privacy Firstな校正ツールを作る
- textlintはJavaScriptで書かれている
- 基本的にルールもJavaScriptで書かれている
- そのため、オフラインでも動作する
- サーバにデータを送る必要がない
textlintをブラウザで動かす
- 目標: grammarlyのtextlint版を作る
- textlintは基本的にはNode.jsで動かしている
- ブラウザで動かすには依存を色々解決するものが立ち
5日でプロトタイピング
- ここから本題 20min
- 5日でtextlint editorを作る
- ブラウザでtextlintを動かす
- ブラウザ拡張であらゆるサイトでtextlintでのチェックを使えるようにする
1日目
1日目

- grammery的なUIを作るプロトタイピング
- あらゆるサイトで動かないと行けないのでWeb Componentsでコンポーネント作成
- → textareaに重ねるようにして要素を置くことで、一部に下線が出るようにするコンポーネント作る


2日目
2日目
textlintのbrowser版はなんでビルドが必要?
- textlintはすべてがpluggable
- textlintのルールを個人に合わせる仕組みが必要
- 同じ人でもルールは文章によって違う
2日目: textlintのbundleをどうするか?
- 方法
- 全部入りのbundleを作る
- 全部オンラインロード
- ルールだけをパッケージするか
2日目: bundle prototype
- 全部入りのbundleを作る
- @zeit/nccでbundle
- → うごいた
- webpackでも動くのは過去からわかってる
- 全部オンラインロード
- PikaCDN, Skypackを使ってルールを読み込む
- → ローカルでのビルドが不要というのが大きなメリット
textlintのbrowser版はなんでビルドが必要?
- Skypackとの格闘
- CDNでのビルド済みファイルの利用
- いくつか対応してunirollでtextlintが動いた!
3日目
npm CDNの問題
- skypackでは
require("pkg/file")
がtranspileされない仕様
pkg
モジュールの相対パスのfile.js
を読み込む仕組み
- Lookup a Package File
- → 現実的にあらゆるルールで担保するのは難しい
webpack as a service
- nccやwebpackでtextlintはビルドできる
- → webpack as a serviceを考える
- codesandboxを使えば、webpackでビルドしたファイルをブラウザだけで取得できそうな予感がした
- どちらにしてもだいたいの人は配布されたファイルを使うだけでいい
- 高度な使い方をする人は自分のtextlintrcを持っているはず → node環境がある
- → つまり textlint + textlintrcからbundleを作成できるコンパイラーを書けば解決できそう
- textlintのルールは仕組み的にdynamic requireで解決している(textlintが依存を知らないため)
- ESLintなど多くのルールをもつツールはだいたい同じ仕組み
- dllみたいなもの = ルール
- @textlint/compilerを作成する
- このルールをcompilerがコンパイルしやすいように、静的な形に整形するlinkerを書く
.textlintrc
(設定ファイル) → staticなrequire
に変換するlinker
- コンパイルの処理自体ははwebpackに生成したコードを投げる
4日目
リアルタイムLint
- ブラウザで動くなら入力ごとのLintしたい
- web workerの対応
- 入力文字における処理はWebWorkerなど別スレッドで行うのが基本
- コメント入力はレイテンシーに弱い 許されるのは 5ms以内ぐらい
- webworkerに対応したtextlintを作成する
@textlint/kernel
(コア)はpure jsで書かれているので、postmessageのラッパーを書くだけで解決
target: self
で解決
kuromoji.jsの辞書解決
- textlintのルールでは一部のルールがkuromoji.jsでの形態素解析を使ってる
- この辞書が圧縮して20mbぐらいある(無圧縮は100mb)
- この辞書を毎回ロードしててる大変
- worker内でキャッシュ、同時に取得した時に1回の取得にまとめる必要がる
kuromoji.jsの辞書ハック
// InMemory Cache
const dictionaryDeferred = new Deferred();
const urlMap = new Map();
BrowserDictionaryLoader.prototype.loadArrayBuffer = async function (url, callback) {
// https://github.com/takuyaa/kuromoji.js/issues/37
const fixedURL = url.replace("https:/", "https://");
const cachedDictBuffer = await dictionaryStorage.get(fixedURL);
if (cachedDictBuffer) {
// console.log("return cache", cachedDictBuffer);
return callback(null, cachedDictBuffer);
}
// Suppress multiple request to same url at same time
if (urlMap.has(fixedURL)) {
return urlMap.get(fixedURL).promise.then(result => {
callback(null, result);
}).catch(error => {
callback(error);
});
}
const deferred = new Deferred();
urlMap.set(fixedURL, deferred);
fetch(fixedURL).then(function (response) {
if (!response.ok){
return callback(response.statusText, null);
}
response.arrayBuffer().then(function (arraybuffer) {
var gz = new zlib.Zlib.Gunzip(new Uint8Array(arraybuffer));
var typed_array = gz.decompress();
return dictionaryStorage.set(fixedURL, typed_array.buffer).then(() => {
// console.log("cached", fixedURL);
deferred.resolve(typed_array.buffer);
callback(null, typed_array.buffer);
});
});
}).catch(function (exception) {
deferred.reject(exception);
callback(exception, null);
});
};
辞書ハック
- prototype hackをしてる
- ちゃんと解決するにはkuromoji.js自体をイジる必要がありそう
- dictionary loaderを外から使えるようにしないとダメそう
- forkしかないのかも
ここまで

5日目
- ブラウザ拡張を作る
- web extension firefox + chromeとかに対応する
- WebExtension Toolboxを採用
- メンテにちょっと不安感あるけど、configを減らして運用でカバー ejectable
Chrome拡張での検証
デザインを書く


デザインを実装する
textlint editor

まとめ
- UIはWebComponents
- コアは最初からPure JavaScriptで書いておく
- ちょっとしたNodeライブラリならwebpackでなんとかできる
- skypackのCDNはRollup系なのできれいなビルドができる
- キレイじゃないライブラリは使えない
- Node.js exportsとかがこれからくるので、きれいな方法がまだ定義されていない
- textlint + ルールを一つにbundleしてオフライン校正ツールが動いた!
これから
- Collaboratorを募集!
- ブラウザで動けば大体のところで動く
- サイトに組み込みしやすい、サーバ側に遅れたデータを管理しなくていい単語
- → 同じ仕組みでウェブサイトに組み込みができる
これから
- frontend
- suggest対応
- ignore対応
- see document対応
- compiler
- codesandboxでコンパイルしたものを配布するテンプレート
- performance
- 今はlintが10msぐらい、文字数に応じて線形的に処理が増える
- ちゃんと考えるなら差分処理が必要だけど、文章にはコンテキストがあるので制限がある
- WebWorkerのおかげでUIにはlatencyは少ないので、1sぐらいならOK
- 現実的にtextareで10kbレベルの文章を書く人は少なくて、ファイルを分けるからなんとかなる
- DOMの最適化が必要。表示してるエリア飲みに情報を表示するなどの最適化が必須
- 拡張
- Chrome拡張にtextlint bundleを更新する仕組みを作る
- textlint.jsをダウンロードしてChrome拡張で使うものを切り替え → Workerを再起動
- storeの公開がめんどうくさいのでどうする?
- ルール: まだ動かないルールもあるはず
おわり