フットサルのワールドカップが終わってしまって、喪失感のあるkitoです。
(リカルジーニョが代表引退にして初優勝&MVP。。。なんてできたストーリーなんだ🙌)

更に夏の終わりも感じてヨルシカの「ただ君に晴れ」をヘビロテして過ごしている今日このごろ🥺

「これから先の人生、躓くことなんて当たり前だ。それでも、ただ君に、晴れぬ空などないことを」

ヨルシカ – ただ君に晴れ

感傷に浸りすぎて、ダークサイドに堕ちないよう前向きに生きます。

今回のブログは、

textlintを利用して文章チェックを行うChrome拡張機能を作成

したので、紹介いたします。

文章のチェック負荷を下げたいよー

開発チームのサポート対応では、誰が対応したとしても、
文章の品質を保った回答とするために、他者による文章チェックを行ってます。
また、この開発ブログ等、各種の文章も他者チェックすることがあります。

ただ、このチェック、、、
確認する人に依存して、指摘内容や気付くポイントが変わってしまう状況になってました。

なんとかしたいよなぁと🥺🥺🥺

人じゃなくても良いことは、機械に任せる

そうです。そこで、 textlint

開発で勃発するソースコード関連の宗教戦争の解決として
機械的な自動フォーマットを用いるのと同様に、文章も機械的にチェックさせてしまおうとなりました。

どうせなら、全社に展開!textlintを簡単に使いたいよー

文章を書くのは、開発チームだけではないので、全社展開も視野に入れました。

ならば、textlint作者様にて提供されている textlint editor で勝てる!と思ったのですが、
導入つまずかないかなぁと。。。

ということで、導入・操作が簡単なChrome拡張機能でも作ってみようと思い立ち、
作ったのがコレ。

(Chromeウェブストアから追加するのみ。文章を入力したら、チェックされて結果表示されるだけ)

ここからは、作った過程の一部を紹介します

Chromeの拡張機能を作ってくよー

開発には、svelte-tailwind-extension-boilerplateを使いました。ありがたや…🙏🙏🙏

まずは、TypeScript化して、

node scripts/setupTypeScript.js

ESLintとPrettierを追加。

npm add -D prettier prettier-plugin-svelte
npm add -D eslint eslint-plugin-svelte3
npm add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

※以下のサイトを参考にESLintとPrettierの設定ファイルとscriptsを追加。ありがたや…🙏🙏🙏

つづけて、Chrome拡張機能のmanifest v3に対応するべく、
rollup-plugin-chrome-extensionを最新にしておきます。

- "rollup-plugin-chrome-extension": "^3.5.2",
+ "rollup-plugin-chrome-extension": "^3.6.2",

あわせてChrome拡張機能のmanifestを v3の形式 に変更します。
※v3の形式にしておくと、Chrome Web Store の審査が早く終わります。

コンポーネントを作ってくよー

特段難しいことは、しておらず。。。
素直に入力用と結果表示のコンポーネントとテストコードを作りました。

  1. 入力用のテキストエリア
    (onKeyupでイベントをdispatch)
  2. 結果件数表示
    1. 表示件数
    2. 結果コピーボタン
  3. lint結果表示部
    1. lintのタイトル
    2. 対象部の内容
    3. 対象の文章

必要となる情報を変数に切り出し、$ をつけてリアクティブな処理にしてあります。
lintの結果に変化があれば、自動的に内容が切り替わります。便利~😇

ブラウザー用にtextlintをweb worker化するよー

ブラウザーでtextlintを利用するために、 @textlint/compiler でweb workerにコンパイルします。

以下のサイトを参考に、いくつかのルールを追加してコンパイルしました。ありがたや…🙏🙏🙏

package.json

"scripts": {
    "compile-textlint-worker": "textlint-script-compiler --output-dir ../../src/third-party --metadataName 'textlint-worker' --metadataNamespace 'http://localhost:3000/' --metadataHomepage 'http://localhost:3000/'"
},
"dependencies": {
  "@textlint/script-compiler": "^0.12.1",
  "textlint-rule-preset-ja-spacing": "^2.2.0",
  "textlint-rule-preset-ja-technical-writing": "^7.0.0",
  "textlint-rule-prh": "^5.3.0"
}

ハマリポイント

textlint-rule-prhで利用する辞書ファイル(yml)内で、別の辞書をimportして読み込んでいると、
web worker使用時にfs関連のエラーとなったので、個別で読み込みしました。

なお、辞書ファイルは、icsmedia社のルールWEB+DB_PRESSのルールを利用してます。ありがたや…🙏🙏🙏

.textlintrc

"prh": {
  "rulePaths": [
     "./prh_rules/icsmedia/prh_idiom.yml",
     "./prh_rules/icsmedia/prh_open_close.yml",
     "./prh_rules/icsmedia/prh_redundancy.yml",
     "./prh_rules/icsmedia/prh_duplicate.yml",
     "./prh_rules/icsmedia/prh_cho_on.yml",
     "./prh_rules/icsmedia/prh_corporation.yml",
     "./prh_rules/icsmedia/prh_web_technology.yml",
     "./prh_rules/WEB+DB_PRESS.yml"
  ]
},

Popupを実装するよー

処理の流れとしては、次の通り単純です。

  1. 文章入力のEventが発生する
  2. textlintを実行する
  3. 結果をコンポーネントに渡す

処理のポイントだけ、以下に抜粋します。

Popup.svelte – script部

(svelte、はじめて書いてます。不安しかない🥺🥺🥺)

let targetText: string;
let lines: string[] = [];

function lintText(event: CustomEvent<string>) {
  targetText = String(event.detail.text);
  lines = targetText === `` ? [] : targetText.split('\n');
}

const textlintWorker = new Worker('../third-party/textlint-worker.js');

let lintStatus: string;
let lintResults: lintMessages;
let errorMessage: string;

textlintWorker.onmessage = (event: MessageEvent) => {
  lintStatus = event.data.command as string;

  switch (lintStatus) {
    case 'init':
      break;
    case 'lint:result':
      lintResults = event.data.result as lintMessages;
      break;
    default:
      lintStatus = '';
      break;
  }
};

textlintWorker.onerror = (event: ErrorEvent) => {
  lintStatus = 'error';
  errorMessage = event.message;
  console.error(event);
};

$: {
  if (lines.length === 0) {
   lintStatus = '';
  }

  if (lines.length > 0) {
    textlintWorker.postMessage({
     command: 'lint',
      text: targetText,
      ext: '.md',
    });
  }
} 

Popup.svelte – 表示部

<!-- textlintする文章 -->
<TextArea on:changedText={lintText} />

{#if lintStatus === 'init' && lines.length > 0}
  <p>...校正中</p>
{:else if lintStatus === 'lint:result'}
  <!-- 校正結果概要 -->
  <TextLintedSummary messages={lintResults.messages} {lines} />

  {#if lintResults.messages.length > 0}
    <!-- 校正結果 -->
    <TextLintedListItem messages={lintResults.messages} {lines} />
  {/if}
{:else if lintStatus === 'error'}
  <p style="color: red">{errorMessage}</p>
{/if}

Chrome ウェブストア に公開するよー

最後にbuildして、Chrome ウェブストアに公開し、社内だけの限定公開としました。
(手続きがなかなか大変。。。笑)

textlintの結果を取捨選択できる文章力が必要であるため、「意識して」読み・書きして文章力を強化するとともに、
実際に使って、効果を検証できればと思っています。

人じゃなくても良いことは、ドンドン機械に任せて効率あげていきたいですね。

それにしても、、、

日本語ってムズカシイ🤷‍♂️

それでは、また。