Parcel+Hyperappでマークダウンエディタを作る

この記事はQiitaに書いていた自身の記事のコピーです。

色々と雑ですがお付き合いください。また、解説は自分のリポジトリのものと少し違います(Linter系バッサリ吹き飛んでます)。

こちら、結果リポジトリ公開したものです。


目次

基本情報

  • Parcelとは
    • 思考停止で(何の設定もなしに)モジュールをバンドルしてくれるマン。
  • Hyperappとは
    • 超軽量データバインドマン。
  • Markdownとは
    • Qiita書くときに使うやつ。

書いていきます

とりあえず必要なものをyarn add(npm install)します。

1
2
3
4
5
yarn global add parcel-bundler
yarn add hyperapp marked node-sass
# これらは以下とほぼ同義です
# npm i -g parcel-bundler
# npm i hyperapp marked node-sass

parcelhyperappは先に書いたとおりです。

  • marked

    • Markdown→HTML変換器くん。
  • node-sass

    • Sass/SCSS→CSS変換器くん。

index.htmlをつくる

最低限のもののみ、HTML5で省略推奨されているタグは含めていません。

1
2
3
4
5
<!DOCTYPE html>
<title>markdown-editor with Hyperapp</title>
<body>
<script src='app.js'></script>
</body>

ここで、app.jsという空ファイルを作っちゃいます。
その後、

1
parcel index.html

localhost:1234を開くととりあえずタイトルが「markdown-editor with Hyperapp」の真っ白のWebページが見えました。
Parcelのすごいところは、とりあえずindex.htmlを引数にして実行すればそこからsrcしてるファイルが全て自動でコンパイル&ブラウザ側でライブリロードされる点ですね。

hello Hyperapp

では、app.jsを編集します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { h, app } from 'hyperapp'

const state = {
'output': 'hello, Hyperapp'
}

const actions = {
}

const view = (state, actions) => (
<main id='app'>
{state.output}
</main>
)

app(state, actions, view, document.body)

1行目はモジュールのインポートですね。hはどうやらappの実行に必要なようなので一緒に入れてます(1回エラー見ました)。
次に、Hyperappではstateactionsviewそれらを適用させる場所(app)が初期化で求められるので、それらを書いてます。

stateは一言で言うと変数です。
actionはそれを編集するためのメソッド、関数を記述します。
viewは表示する中身をほぼそのまんま書いていきます。中身はJSX記法と呼ばれるもので、**{}で変数の中身を表示できます**。

ので、これを保存した頃にはブラウザ側では「hello, Hyperapp」と表示されているはずです。

textareaの中身をリアルタイムで表示する

流れとしては、viewを編集して<textarea>タグを追加、それが編集されたらactionからメソッドを呼んでstateのoutputをいじって表示する感じです。

で、ハマったのはtextareaの中身をどうやって取得するかです。
Vue.jsだと、v-modelとか便利なものがあるんですが、Hyperappに似たようなものがあるかと思ったらなかったりしました。

見つかりませんでしたので、生Javascriptで書いてます。誰か知ってたら教えてください。
先程書いたコードの一部を書き換えていくような形で書いていきます。

1
2
3
4
5
6
const view = (state, actions) => (
<main id='app'>
<textarea id='editor' oninput={e => actions.setOutput(document.getElementById('editor').value)} />
<div>{state.output}</div>
</main>
)

oninputで入力を感知したら、document.getElementById('editor').valueで得られた文字列(=textareaの内容)を引数にactions.setOutputを実行します。
では、actions.setOutputを定義します。

1
2
3
const actions = {
setOutput: (input) => state => ({ output: input })
}

アロー関数式=>を使っているので混乱させてしまったら申し訳ないですが、つまり引数をinputとして扱ってstate.outputを変更するというものです。

この状態で、ブラウザに表示されているtextareaに文字を入れたとき、その内容がそのまま表示されていれば成功です。

textareaの中身でリアルタイムHTMLプレビューする

さっきまでの文字列はあくまで文字列として扱われ、HTMLタグはプレビューできてません。
上で話した通り、モジュール「marked」はMarkdownをHTMLに変換するため、HTMLプレビューが必要でした。

この変更はviewを編集するだけで秒解決です。ただ、Vue.jsで言うところのv-htmlのような機能も見当たらなかったので、直書きします。

1
2
3
4
5
6
const view = (state, actions) => (
<main id='app'>
<textarea id='editor' oninput={e => actions.setOutput(document.getElementById('editor').value)} />
<div id='preview' innerHTML={state.output}></div>
</main>
)

innerHTMLに入れたら解決しました。やったぜ。

仕上げの時間だ。Markdownプレビューしますよ

さて、もうここまでくるとoutputの中にmarkdownから変換されたHTMLを代入するだけですね。
上に書いたとおり、Markdown→HTML変換器くんのmarkedを使います。

まずmarkedのimport文の追加。

1
import marked from 'marked'

で、actionの編集。

1
2
3
const actions = {
setOutput: (input) => state => ({ output: marked(input) })
}

marked超便利。marked超便利。marked超便利。
はい、これでとりあえず終了ですね。

見栄えをどうにかする

これに関しては最初にnode-sassを導入してるのでSCSSやSass、もしくはCSSを書いて下さい。僕の場合はこんな感じでした。
今回はJavascriptの解説だったので、あえてこの解説は省きます。

style.sass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
html, body
height: 100%
width: 100%
margin: 0

#app
display: flex
height: 100%

textarea, #preview
flex: 1 1 0
overflow-y: scroll

textarea
padding: 1em
font-size: 12px
background-color: #eee
line-height: 1.5em
font-family: courier, monospace
border: 0

#preview
padding: 1em

pre
padding: 15px
background-color: #282c34
color: #EEE
margin-right: 10px

@media print
textarea
display: none
#preview
overflow-y: visible

で、それをimportします。

1
import './style.sass'

おまけ:Highlight.jsでcodeをいい感じに表示する

Highlight.jsはcodeタグ内の各種コードをいい感じの色にしてくれるものです。

yarn add(npm install)

1
yarn add highlight.js
1
2
3
4
5
6
7
8
import highlight from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'

marked.setOptions({
highlight: function (code, lang) {
return highlight.highlightAuto(code, [lang]).value
}
})
1
2
.hljs
all: unset

以上です。お疲れ様でした

一分説明が雑になってしまったので申し訳ないですが、以上になります。
Parcelは設定ファイルを作成しなくて良いHyperappは非常に少ないコードで書けるというそれぞれの強みが有ります。
逆に、これらは細かい設定が出来ない、複雑なものが作りにくいという弱みに直結しますが、使えるところでは使えるということを体感して頂けたら幸いです。

あ、Parcelで静的データ書き出ししちゃいましょう。

1
parcel build src/index.pug --public-url ./

これでdistの中に公開できる状態のデータが生成されているはず。
はやい。
かんたん。
Parcelだいすき。
何か質問、ご指摘等有りましたらなんでもお願いします。
では。

修正・補足

  • 180322
    • parcel-bundlerをグローバルインストールに変更しました。グローバルを汚したくない人はnpm scriptsをよしなに書いて下さい。