この投稿は、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の拡張を作ったりしたら、もっと便利になるかと思います。