sKawashima

Development

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

#hyperapp#Marked#highlight.js#Parcel#JavaScript

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

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

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


基本情報

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

書いていきます

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

terminal
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で省略推奨されているタグは含めていません。

index.html
<!DOCTYPE html>
<title>markdown-editor with Hyperapp</title>
<body>
  <script src='app.js'></script>
</body>

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

terminal
parcel index.html

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

hello Hyperapp

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

app.js
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で書いてます。誰か知ってたら教えてください。 先程書いたコードの一部を書き換えていくような形で書いていきます。

app.js
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を定義します。

app.js
const actions = {
  setOutput: (input) => state => ({ output: input })
}

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

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

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

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

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

app.js
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文の追加。

app.js
import marked from 'marked'

で、actionの編集。

app.js
const actions = {
  setOutput: (input) => state => ({ output: marked(input) })
}

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

見栄えをどうにかする

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

style.sass

style.sass
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します。

app.js
import './style.sass'

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

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

yarn add(npm install)

terminal
yarn add highlight.js
app.js
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
  }
})
style.sass
.hljs
  all: unset

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

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

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

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

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

修正・補足

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