esa.ioのエディタにリンター機能を付加する実験の記録


この投稿は、esa.ioのエディタをハックして、リンター機能を付与できないかを検証した記録です。

esa.ioはチーム向けドキュメント共有サービスで、Markdownでドキュメントを書けることからQiitaと使い勝手が似ているウェブサービスです。

リンター機能とは

リンターはプログラムコードの良くない書き方を指摘してくれるツールです。たとえば、JavaScriptならeslintが有名です。

Markdownを対象としたリンターもあります。markdownlintやremark-lintがそれです。これらを使うと、Markdownの書き方で良くないところを指摘してもらえます。

日本語の校正を目的とした、textlintというのもあります。これは日本語の表現で直すべきところを指摘してくれるツールです。

なぜリンター機能を付与したいか

複数人でドキュメントを書いていると、書き方で気になるところが出てきます。たとえば、ドキュメント同士のリンクの書き方です。Markdownでサイト内リンクのURLを指定する方法として、https://から書き始める方法と、ドメイン部分を省略して絶対パスで書く方法があります。esaでは絶対パスで書くほうが望ましいです。ドメイン名はいつでも変更できるので、もしドメイン名が変わるとリンク切れになります。

[リンク](/posts/123)[リンク](https://example.esa.io/posts/123)

他にも、リスト記法を-*どちらで統一するとか、インデントは4スペースにするとか、細かい統一感が出したかったです。そのために以前、esa Webhookを使ってMarkdownを整形するツールを作ってみたりもしました。これはこれで便利なのですが、書いているときにフィードバックが無いのが課題でした。

そこで、esaエディタにリンター機能をつけられたらいいなと思いました。

esaエディタの仕様

esaのエディタはCode Mirrorというオープンソースのエディタをベースに作られています。リンター機能をつけるには、Code Mirrorをいじる必要があります。

esaエディタインスタンスのありか

Code Mirrorにふれるには、Code Mirrorのオブジェクトを取り出さないとなりません。これをするには、次のようなコードで.CodeMirror要素からCodeMirrorプロパティを参照するだけです。

const editor = document.querySelector(".CodeMirror").CodeMirror

Code Mirrorのリンター機能

Code Mirrorにはlint.jsというリンター機能のフレームワークになるアドオンがあります。これを利用するのが、esaエディタにリンターをつける近道です。

esaエディタにリンターをつける方法

lint.jsをesaの投稿ページにロードしてやれば、基本的にリンター機能のインストールができるのですが、lint.jsはグローバルオブジェクトにCodeMirrorプロパティを必要としているので、esaエディタのCodeMirrorオブジェクトをグローバル変数化してやる必要があります。注意点なのですが、lint.jsが依存するCodeMirrorオブジェクトはエディタのインスタンスではなく、コンストラクタです。なので、上で取り出したeditorをグローバル変数化しても意味がありません。esaのCodeMirrorコンストラクタはスコープが閉じたところにあるので直接アクセスできません。そこで、editor変数からコンストラクタを取り出す方法で対処します。

window.CodeMirror = editor.constructor;

これでlint.jsをロードするための条件がととのいます。あとは、次のファイルを動的にロードしてやればOKです。

最後に、エディタインスタンスeditorにリンターを追加するとリンターが動くようになります。

function lint(text) {
  return [
    {
      severity: "error",
      from: { line: 0, ch: 11 },
      to: { line: 0, ch: 40 },
      message: "リンクにはドメインを含めないようにしましょう。例: /posts/123",
    },
  ];
}

// setup gutter
editor.setOption("gutters", ["CodeMirror-lint-markers"]);
document.querySelector(".CodeMirror-lint-markers").style.width = 0;

// load addon
await loadJavaScript("https://codemirror.net/addon/lint/lint.js");
await loadStylesheet("https://codemirror.net/addon/lint/lint.css");

// enable lint feature
editor.setOption("lint", lint);

esa.ioのエディタにリンター機能を付加する技術的な解説は以上です。

完成版のコード

ここまでの手順をコードにしたものが次になります。これをGoogle ChromeのDevToolのコンソールで実行すると、1行目の11文字目〜40文字目に警告が表示されます。

main();

async function main() {
  const editor = discoverCodeMirrorInstance();
  globalifyCodeMirrorClass(editor);
  await enableLinter(editor, lint);
}

function lint(text) {
  return [
    {
      severity: "error",
      from: { line: 0, ch: 11 },
      to: { line: 0, ch: 40 },
      message: "リンクにはドメインを含めないようにしましょう。例: /posts/123",
    },
  ];
}

function discoverCodeMirrorInstance() {
  return document.querySelector(".CodeMirror").CodeMirror;
}

function globalifyCodeMirrorClass(editorInstance) {
  window.CodeMirror = editorInstance.constructor;
}

async function enableLinter(editor, lint) {
  // setup gutter
  editor.setOption("gutters", ["CodeMirror-lint-markers"]);
  document.querySelector(".CodeMirror-lint-markers").style.width = 0;

  // load addon
  await loadJavaScript("https://codemirror.net/addon/lint/lint.js");
  await loadStylesheet("https://codemirror.net/addon/lint/lint.css");

  // enable lint feature
  editor.setOption("lint", lint);
}

function loadJavaScript(url) {
  return new Promise((resolve) => {
    const script = document.createElement("script");
    script.src = url;
    script.addEventListener("load", resolve);
    document.head.appendChild(script);
  });
}

function loadStylesheet(url) {
  return new Promise((resolve) => {
    const link = document.createElement("link");
    link.rel = "stylesheet";
    link.type = "text/css";
    link.href = url;
    link.addEventListener("load", resolve);
    document.head.appendChild(link);
  });
}

このコードをベースにGoogle Chromeの拡張を作ったりしたら、もっと便利になるかと思います。