今更ながらマークダウン記法について学び、とても感動しています。
最近はメモや簡単な文章作成にはマークダウンを使っています。
そんな時、自分の理想的なマークダウンエディタがブラウザ上で動けばもっと便利になると思い、少し試してみました。
修正点やもっとこうした方がいいなどございましたらご教授いただけると幸いです。m(_ _)m
どんなエディター?
僕がイメージしているエディターは、よくある画面が二分割されてエディター部分とプレビュー部分に分かれているタイプでは無く、書いた部分がそのままHTMLに変換されるものをいいます。
有名なソフトだと、Typora
などがあるでしょうか。
ざっと調べてもいろいろな実現方法があるみたいですが、今回は一番単純なようで実は大変そうなcontentEditable
とexecCommand
を用いた実装について試行錯誤しながら試してみました。
contentEditableの仕様
実装に当たってまずはcontentEditable
の仕様を調べてみます。
以下のファイルでテストをしてみました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Test</title>
</head>
<body>
<div contenteditable='true'>
<h1>h1</h1>
<h2>h2</h2>
<div>div</div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<blockquote>block quote</blockquote>
</div>
</body>
</html>
マークダウン記法でいうと、文頭に所定のマークを入れる要素たちです。
とりあえずここでこのページの簡単な挙動を調べることができます。
実際にお試しいただけるとわかりやすいかと思いますが、特徴をまとめるとこんな感じです。
なお、僕の環境では最新版のChromeをMac上で使っています。
良さそうな点
-
Shift+Enter
では同じエレメント内で改行、Enter
のみだと今のエレメントの次にdiv
が挿入される - リストを
Enter
のみで改行すると次の行にもリストが自動で挿入される。二度Enter
を押すと通常のdiv
が挿入される
少し気になった点
-
blockquote
ではEnter
のみを押しても、div
の代わりにblockquote
が挿入され抜け出せない。
こんな感じでしょうか。
意外と標準機能でマークダウンエディターっぽい動きができている気がします。
あとはマークダウン記法を読み取って、その都度DOMを置き換えればいいような気がしていました。
この時までは...
とりあえず作ってみた
マークダウン記法の判定
さて、先述の特徴を踏まえて、文頭数文字で判断できる系の以下の四つを実装してみます。
- 見出し:#, ##, ###, ...など
- リスト:-
- 番号付きリスト:1.
- 引用文:>
キャレットのある位置行の文字は以下のコードで取得できます
window.getSelection().getRangeAt(0).endContainer.data
keyup
イベントが起こるたびに現在の行の文字列を取得し、マークダウン記法になっているかを判定します。
const element = document.getElementById('markdown')
element.addEventListener('keyup', function (event) {
const currentLine = window.getSelection().getRangeAt(0).endContainer.data
if(currentLine.match(/^#{1}\xA0$/)){ // 見出し
// '# 'を<h1>に置き換える
}
//
// 見出し2〜6は{}内の数字を変えるだけ
//
else if (currentLine.match(/^>\xA0$/)){ // 引用
// '> 'を<blockquote>に置き換える
} else if (currentLine.match(/^\d+\.\xA0$/)) { // 順序付きリスト
// '- 'を<ul>に置き換える
} else if (currentLine.match(/^[\-+*]\xA0+$/)) { // リスト
// '1. 'を<ol>に置き換える
}
})
はい、これで場合分けができました。
マークダウン記法の変換
contentEditable
ではEnter
を押すと改行されてdiv
が挿入されます。
このdiv
をマークダウン記法に対応するDOMに変更するためにexecCommand
を用います。
以下は<h1>に変換する例です。
document.execCommand('formatblock', false, 'h1')
第一引数のformatblock
はMDN web docsによると、
formatBlock
現在の選択範囲を含む行の前後に HTML ブロックレベル要素を追加し、すでに存在する場合は、その行を含むブロック要素に置き換えます (Firefox では <blockquote> は例外です。 — これはブロック要素を囲みます)。引数としてタグ名の文字列が必要です。実質的にすべてのブロックレベル要素を利用することができます。
(Internet Explorer および Edge は見出しタグ H1–H6, ADDRESS, PRE のみに対応しており、 "<H1>" のように山かっこで囲む必要があります。)
とのことです。
要するに、今キャレットがあるDOMを指定の要素に変換するってことだと思います。
(詳しくはわからないので、ご教授いただけると幸いです。)
なお、リストと順序付きリストは別のコマンドを用います。
それぞれ以下のようになります。
// リスト
document.execCommand('insertUnorderedList')
// 順序付きリスト
document.execCommand('insertOrderedList')
マークダウン記法の削除
さて、前述までの方法でマークダウン記法を判定して、HTML要素の変換がすることができましたが、文字として打ち込んだマークダウン記法は残ったままです。
これを削除します。
要素内のテキストを変更するといえば、innerText
が思いつきます。
キャレットがある要素のinnerText
を取得することでうまくいきそうです。
先述したwindow.getSelection().getRangeAt(0).endContainer
では現在のテキスト部分、つまり#text
が取得されます。
この親要素が現在の要素になります。
すなわち、マークダウン記法を削除するには以下のコードを用います。
window.getSelection().getRangeAt(0).endContainer.parentNode.innerText = ''
とりあえずできた
以上のことをまとめると以下のコードになりました。
const element = document.getElementById('markdown')
element.addEventListener('keyup', function (event) {
const currentLine = window.getSelection().getRangeAt(0).endContainer.data
if(currentLine.match(/^#{1}\xA0$/)){ // 見出し
document.execCommand('formatblock', false, 'h1')
clearCurrentLine()
}
//
// 見出し2〜6は{}内の数字を変えるだけ
//
else if (currentLine.match(/^>\xA0$/)){ // 引用
document.execCommand('formatblock', false, 'blockquote')
clearCurrentLine()
} else if (currentLine.match(/^\d+\.\xA0$/)) { // 順序付きリスト
document.execCommand('insertOrderedList')
clearCurrentLine()
} else if (currentLine.match(/^[\-+*]\xA0+$/)) { // リスト
document.execCommand('insertUnorderedList')
clearCurrentLine()
}
})
const clearCurrentLine = () => {
window.getSelection().getRangeAt(0).endContainer.parentNode.innerText = ''
}
デモはこちらから
次回に向けての修正点
さて、デモをお触りいただくとわかると思いますが、このコードChromeではうまく動きませんorz
(Safariでは動きました。他のブラウザは未検討)
要素は挿入されますが、キャレットが直前の要素に飛んでしまい、しかも挿入された要素にはどうやってもキャレットを合わせることができません。
これを踏まえた以下が次回に向けての修正点です。
- 挿入された要素にキャレットを合わせることができない
-
blockquote
はEnter
を押しても抜け出すことができない
次回はこれらの問題点を修正するところから始めます。
アドバイスなどございましたら、コメントください。m(_ _)m