textlint

textlintはMarkdownなどテキスト向けのLintツールで、テキスト版ESLintみたいな感じのツールです。

最近azu/JavaScript-Plugin-Architectureという小さな書籍を書いていて、色々簡単に使えるような仕組みを追加しています。

この記事では簡単なtextlintの導入方法について紹介します。

扱うtextlintv3.3.0以降とします。

textlint logo

一部ルール(主に英語ですが)のオンラインデモページはこちらにあります。

手軽に動作を見たい場合は、Chrome拡張版を試すといいかもしれません。 こちらを使うとNode.jsなどの環境を揃えなくても試せます。

textlint-appというクロスプラットフォームで動く単独のアプリ版もあります。 .textlintrcの設定方法は共通しているので理解する必要がありますが、Node.js環境が内蔵されているので別途Node.jsのインストールは不要です。

インストール

textlintはデフォルトでは一つもルールがありません。 これはpluggable linting toolのためでもありますが、現実的に全ての言語(日本語やロシア語といった言語)で上手く機能するようなルールは少ないと思ってるからでもあります。(逆にtextlintは日本語に特化したツールでもありません)

ルールはJavaScriptで書くことができ、それらのルールはNode.jsのパッケージ管理ツールであるnpmで公開、利用することができます。 (textlintのルールはtextlint-rule-*という名前で公開をオススメします)

なので、自分が作った幾つかのルールを入れて試してみたいと思います。

npmでグローバルにtextlintと3種類の日本語関係のルールを入れてみます。

npm i -g textlint
npm i -g textlint-rule-max-ten textlint-rule-spellcheck-tech-word textlint-rule-no-mix-dearu-desumasu

Node.jsのpackage.jsonがあるプロジェクトなら--save-devとかでインストールして使うと良いと思います。(できればこちらをオススメします)

インストールすると$ textlintというコマンドが使えるようになります。

入れた3つのルールはそれぞれ以下のようなことをチェックしてくれます。

文章をルールでLintする

先ほど入れたルールを使ってみましょう。

--rule <ルール名>でルールを指定でき、ルール名とはインストール時のパッケージの名前となっています。

textlint-rule-*で始まるパッケージ名は省略できるようにしてあるので、具体的には次のようになっています

  • textlint-rule-no-mix-dearu-desumasu -> no-mix-dearu-desumasu
  • textlint-rule-max-ten -> max-ten
  • textlint-rule-spellcheck-tech-word -> spellcheck-tech-word

README.mdというファイルをこれらのルールでチェックしたい場合は以下のように書くことができます。

$ textlint --rule no-mix-dearu-desumasu --rule max-ten --rule spellcheck-tech-word README.md

result

引数が多いですね…

textlint v3.3.0.textlintrcという設定ファイルをサポートしていて、上記のコマンドは以下のような設定ファイルにすることができます。

設定ファイルはJSON、YAML、JSモジュール(module.exports = {})で書くことができます。(ファイル名は .textlintrc で同じ)

.textlintrc

{
  "rules": {
    "max-ten": {
      "max": 3
    },
    "spellcheck-tech-word": true,
    "no-mix-dearu-desumasu": true
  }
}

rulesにはルールの有効(true)/無効(true) または ルールの設定を書くことができます。

詳しくはREADME.md#textlintrcを見てみて下さい

textlintを実行すると自動で.textlintrcファイルを探索して読み込まれるので、--rule引数を指定しなくてもよくなります。

$ textlint --rule no-mix-dearu-desumasu --rule max-ten --rule spellcheck-tech-word README.md
# ==
$ textlint README.md

実例: refactor: use .textlintrc for textlint by azu · Pull Request #43 · azu/JavaScript-Plugin-Architecture

textlintのルールは以下のWikiにまとめてありますが、ルールを作った場合は自由に追加してみてください。

追記:

どのルールを使えばいいか分からない!という場合はルールプリセットから始めるとよいでしょう。

表記揺れの辞書をプロジェクトに入れたい場合はprhが便利です。

textlintの設定をESLint configのようにnpmで共有することもできます。

ルールによっては --fix で自動修正に対応しています。 導入した時にエラーが多い場合はこの辺の修正を適応するところから始めるといいかもしれません。

日本語でルールなどについて話せるGitterのチャットルームが以下にあります。

  • Gitter

ルールを作る

textlintのルールの作り方は以下のドキュメントに書かれています。

Lintの仕組みはESLintと同じく、Markdown(コード)をパースしてASTにしたものをtraverseしながらそれぞれのルールに渡してチェックする仕組みをtextlintは提供しています。

詳しくはESLintのプラグインアーキテクチャを解説を書いたので読んでみるといいと思います。

以下の記事でも簡単に紹介しています。

例えば、"しかし"という同じ接頭辞が連続して出てくるのをチェックしたいとします。

しかし、〜。
だが、〜。
しかし、〜。

これをチェックするtextlintのルールは以下のように書けます。

  • それぞれのパラグラフの接頭辞を取り出す
  • 変数に接頭辞を保存しておく
  • 2センテンス以内に同じ接頭辞が使われないかを調べる
  • 一致していたらcontext.reportでエラー報告をする
import ObjectAssign from "object-assign"
const defaultOptions = {
    interval: 2
};
const punctuation = /[。\?]/;
const pointing = /[、,]/;
function splitBySentence(text) {
    return text.split(punctuation);
}
// conjunction
function getFirstPhrase(sentence) {
    var phrases = sentence.split(pointing);
    if (phrases.length > 0) {
        return phrases[0].trim();
    }
}
export default function (context, options = {}) {
    options = ObjectAssign({}, defaultOptions, options);
    let {Syntax,getSource, report,RuleError} = context;
    var previousPhases = [];
    var useDuplicatedPhase = false;
    return {
        // パラグラフ == <p> と考えればいいはず
        [Syntax.Paragraph](node){
            // パラグラフは1文だけとは限らないので。で分割
            var text = getSource(node);
            var sentences = splitBySentence(text);
            sentences.forEach(sentence => {
                // 先頭の接続詞、 をとりだす。
                var phrase = getFirstPhrase(sentence);
                if (phrase.length === 0) {
                    return;
                }
                // 少し前の文で出てきてないかをチェック
                if (previousPhases.indexOf(phrase) !== -1) {
                    useDuplicatedPhase = true;
                }

                // 使われてたらエラー報告
                if (useDuplicatedPhase) {
                    report(node, new RuleError(`don't repeat "${phrase}" in ${options.interval} phrases`));
                    useDuplicatedPhase = false;
                }
                // add first item
                previousPhases.unshift(phrase);
                previousPhases = previousPhases.slice(0, options.interval);
            });
        }
    }
}

このルールはazu/textlint-rule-no-start-duplicated-conjunctionに置いてあります。

おわりに

textlintはデフォルトでルールを持ってない代わりに、自分でルールを書きやすいようにしています。

最近だとRedPenもJavaScriptでバリデーションを書けるようになったりしています。(MarkdownだけじゃなくてAsciidocやLaTeXも対応してる)

なので、自然言語に詳しい人がもっと難しい日本語文法などの間違いをチェックできるものを書いていったり、思いつきで自分で書いた文章をチェックするものを作ってみると面白くなるんじゃないかなと思います。

複雑なことを考えなければ、正規表現とStringメソッドぐらいで、ある程度のルールを書けたりします。(よく使うパターンはazu/textlint-rule-helperに追加していきたい)

自然言語をチェックする場合に万人が満足するルールを作るのは難しそうなので、自分用のルールを書いてみて、必要に応じてオプションを増やすのがいいのではないかなーと思います。

最近だとJavaScriptでも日本語をちゃんと扱える形態素解析器であるkuromoji.jsrakutenmaなどがあります。

追記(2016-05-22): textlintの日本語文法を扱うルールの多くはkuromoji.jsのラップであるkuromojinを使っています。(読み込む辞書のキャッシュため)

また、日本語がそのまま扱えるわけじゃないですがNaturalNode/naturalwooorm/retextなど結構手軽に文章などを扱って遊べます。

特にwooorm先生はALEXというチェックツールを出したり、Markdownパーサであるmdastを作ったり、テキスト処理向けの抽象フォーマットであるVFileを作ったり精力的にモジュールなどを書いています。

この辺の抽象フォーマットなどの足並みが揃ってくると、文章を扱うツールのエコシステムが回ってきて色々なツールが出てくるようになってくると思います。

例えば、textlintではMarkdownはazu/markdown-to-astでパースして独自に定義したAST(抽象構文木)にした状態で扱います。 しかし、独自のASTだとファイルフォーマット毎に独自のパーサが必要になり、そのASTを扱うツールもそれぞれ必要になってしまうため結構面倒くさいです。

その辺が解決していければ、プログラミング言語を扱うツールのように、文章を扱うツールがもっと作りやすくなったりするんじゃないかなと思います。

文章の正しさは時間で変化する感じがして、その時に正しいと思ったことをすぐにチェックできるようにしたいと思ってtextlintを作ったので、一度遊んでみるといいのかもしれません。