ジンジャー研究室

長めのつぶやき。難しいことは書きません。

inline style で :hover を再現した

CSS in JS(Elm)方式で、:hoverが出来ない問題を解決した。

github.com

ElmだけどJavaScriptでもたぶん出来る。

使い方

通常、次のように書いている部分を

li [] [ text "Hello" ]

次のように書く。

hover [("background", "#456")] li [] [ text "Hello" ]

すると、ホバー時のみ追加のスタイルが適用される。

仕組み

onmouseenteronmouseleaveの2属性にスクリプトを自動的に突っ込む。

onmouseenter時に、元々DOMに付与されていたスタイルをdata-hover-style-nameに退避して、ホバー時のスタイルを適用する(キーはキャメルケースに変換する)。onmouseleave時に退避していたスタイルを元に戻す。自分の要素しかいじっていないので、Virtual DOM的にも問題ない。

詳しい実装はソースで。

気付き

onmouseenteronmouseleaveイベントはマウスが動いたときには発火するんだけど、要素の側が動いてマウスと重なったりする場合に発火されないことに気付いた。詰んだと思ったんだけど、調べてみたら実は:hoverも同じ挙動をしていた問題なし。

検証用: https://jsfiddle.net/86vc591j/

海外エンジニアが話題にしていて「なるほど」と思ったプログラミングに関する考え方3つ

f:id:jinjor:20160603011250p:plain

プログラミングに関する格言みたいなのは昔から結構あって、例えばYAGNIみたいに日本でも十分浸透してるのは多いんだけど、やっぱり新しい概念はどんどん生まれていくので追いかけていると面白い。

というわけで、最近知った中でもっと日本でも言及されても良いと思ったやつを3つ紹介。

Simple Made Easy

Rich Hickey(Clojure言語の作者)による講演(2011年)のタイトル。全文はここで読める。英語しんどくてPOSTDに投げたんだけど音沙汰がない。まだ全部見てないから和訳欲しい。

内容としては、みんな安易に「簡単」なものを選びがちだけど「シンプル」なものの方が価値あるぜ、というもの。曰く、「シンプル」は絶対的・客観的な指標だけど「簡単」は相対的・主観的なもの。例えば英語の話者にとってドイツ語は難しいが、それは自分にとって「遠い」存在であるだけで悪いものじゃない。

「慣れているか」よりも「シンプルか」に目を向けよう、という教訓。

Magic vs. Boilerplate

上と似た系統の話題なんだけど、「マジック」と「ボイラープレート」はトレードオフの関係にあるという話。Phil Webb氏(Spring Frameworkのコミッター)下のツイートが発端らしい。

http://www.johndcook.com/blog/2016/03/07/the-magic-vs-boilerplate-tradeoff/

直接どっちが良いかという答えは書かれてないけど、周りの反応を見ている限りでは「マジックよりもボイラープレートを取れ」という主旨で良さそう。冗長な記述を省略するために抽象化を進めるのは正しいんだけど、行き過ぎるとマジックになって追うのが困難になる。これについて言及しているブログは、例えばこことか。

SSCCE

Short, Self Contained, Correct (Compilable), Exampleの頭文字をとったもの。公式サイト?らしきところに詳しい解説がある。

何かというと、バグ報告の時に「このケースで上手く動作しない」と言って数百行のコードを渡されたらウンザリするよね、ということで例はなるべく小さくしろという話。そのバグが再現する条件を満たす最小限のコードであることが理想で、報告された側がすぐに試せるように無駄な依存関係も全部切って即座にコンパイル可能な状態で渡すと喜ばれる。

この単語をよく見かけるのが主にElmコミュニティなので、言語とか小さいライブラリとかに向いていて大規模に環境を用意する必要があると厳しい感じはするんだけど、心がけとしては持っておくと良いと思う。

まとめ

何かあった時に自分の言葉で一から説明するよりも、こういうのをさっと引用すると説得力が増しそうな気がするので便利に使っていきたい。

「Webアプリ」の解釈が広すぎる話

最近Webフレームワーク周りで無駄に摩擦が生まれてるなー、と思うことを詩的に書いてみる。

そもそも何が作りたいのか

古くはjQueryから始まって、最近だとReact(+Redux)とかAngular2とか色々あるわけだけれども、そもそもそれらを使って作ろうとしてるものはみんな一緒なの?っていうのがあって、色んな話を聞いているとかみ合ってない感がすごい。以下の分類は別に細かくちゃんと定義しましょうとか言っているわけではなくて、「例えばこういうのがあるんじゃないの?」という一例。いま自分が関わっているのは主に3と4なので、その他で間違ってたら指摘して欲しいんだけど、この前提を共有していないために「複雑すぎる」とか言ってるんじゃないかという仮説がある。

1.Webサイト

基本的に静的なWebサイトで画面遷移するんだけど、ところどころ動きがあったりするのでフレームワークが必要。SEOが重要なのでサーバーサイドレンダリングしたい。初期レンダリングの速度がそのまま売上につながる。モバイルも考えてファイルサイズは極力小さくしたい。

2.Webシステム/サービス

SNSとか動画サイトとか。チャットがあったりタグシステムがあったり、一画面で色々したい。SEOについては上と同じ。初期レンダリングは若干遅くても何とかなるが、UX向上のためにやはり速くしておきたい。

3.業務Webシステム

閉じたWeb。SEO検索エンジンを付けたときの利便性のためでオプションだが、特定の情報にリンクするためのURLは生成したい。初期レンダリングの速度はUX向上のために必要だが、直接的には売上に貢献しないかもしれない。デザインは全体で統一感さえ取れていればなんとかなる。

4.GUI / エディタ

表計算ソフトとかテキストエディタとかIDEとか。SEO関係ない。セキュリティ要件をクリアしつつ、GUIを構築する手段としてWebを使いたい。コピー/ペースト・Undo/Redoコンテキストメニューなどは当たり前の世界。状態をたくさん持つし、コンポーネントもネストしまくる。

5.ゲーム

Canvas用のフレームワークもあるが、DOM用のフレームワークを便利に使いたい。SEOはトップページで必要(投稿できるタイプなら各ページに必要かもしれない)。これも状態をたくさん持つ。

「SPA」は本当に1ページなのか

「Yes」という答えがあるのかもしれないが、個人的にはそういうのはあんまり見たことがなくて、全く別の内容を扱っている場合は普通にページを分けている(ログインページとかは大体分けるし)。その場合は「Router」の役割はそこまで重要じゃなくて、せいぜい検索クエリが最初からセットされてるくらいの機能で良い。だから「Routerとか大げさじゃね?」という意見にも頷ける。

どれだけ未来を見ているか

これも人によって大きく異なると思っていて、明日使うために便利な道具を探す人もいれば、理想の開発を求めて将来に投資する人もいる。自分は後者に寄っているけれども、見方によっては馬鹿げてるとは思う。人によってこの距離感が違うのが、すれ違いの要因のひとつであるように思う。「俺的未来仕様」を考えすぎじゃないかっていうフレームワークもあるので、批判があるのも分かる。

どうすべきか

ただ感じだことを書いただけなので、別にどうでもいい。たまに自分とは全然違う角度から切り込んでくるブコメとかがあって「そういう観点あるなー」とためになったりするので(感謝!)、異文化が混ざってるのは必ずしも悪くない。ただ、他人のブログとかを読むときに「はぁ何考えてるの?」と思ったりする前に立ち止まって考えようと思った。そういう話でした。

CSS in JS(Elm)したら想像以上に良かった

f:id:jinjor:20160530165006p:plain

追記

最新の感想も合わせてご覧ください。

jinjor-labo.hatenablog.com


React界隈では結構前からCSS in JS」と言って、雑に言うと「CSSはイケてないからJSでインラインスタイルを書いてしまえ」という話がある。(ちゃんと知りたい人はこちら

自分も前々からCSSは変数が使えないとか名前が被るとか諸々イケてないのは同意してたんだけど、じゃあJSで書くのが良いかと言われたら「いや流石にロジック汚れるんじゃね?」とか「CSSの便利機能を捨てて平気なの?」とか色々と懐疑的だったんだけど、1~2か月書いてみたら想像以上に良かったので感想を書くことにした。

まず一番に主張したい部分を先に言うと、こう。

(誤解)JSのコードがスタイル記述で汚れる
(正解)JSのコードがスタイル記述から解放される

前提

  • 実際に書いたのはJavaScriptではなくElmなので以下は全てElmコードで書くんだけど、本質は変わらないはずなのでJavaScriptに置き換えて読んでください。
  • デザイナと協業したりプロダクションで実際に使ったわけではないので、その辺のノウハウはないです。
  • パフォーマンスの話はないです。遅くなったら考えようと思ったけど遅くならなかったので。
  • CSSに対する問題意識はある程度共有できていて、React的な書き方をしているWebアプリが対象。

というわけで、ここからが感想というかやってみて分かったこと。

スタイルは今まで通り分離できる

まず誤解されているかもしれないところを解消する。「CSS in JS」と言っても、メインのロジック中にベタベタスタイルを記述していくわけではなくて、スタイルは普通に分離して書くということ。基本的には今までstyle.cssに書いていた内容をstyle.jsに書くだけ。

Elmではこんな感じで「Styles.elm」を記述した。雰囲気を見てもらうのが目的なので詳細な内容は追わなくてOK。

module Styles exposing (..)

zIndex =
  { subView = "600"
  , messageBar = "700"
  , contextMenu = "800"
  }

messageBar =
    [ ("position", "absolute")
    , ("color", "#fff")
    , ("z-index", zIndex.messageBar)
    , ("padding", "5px 10px")
    , ("transition", "height opacity 0.8s linear")
    ]

successBar =
  messageBar ++
    [ ("background-color", "#4c5")
    , ("opacity", "1")
    ]

errorBar =
  messageBar ++
    [ ("background-color", "#d45")
    , ("opacity", "1")
    ]

noneBar =
  messageBar ++
    [ ("opacity", "0")
    , ("pointer-events", "none")
    , ("background-color", "#4c5")
    ]

CSSを書いてるのと何ら変わらない。使用する側のコードは以下。

successView msg =
  div [ style Styles.successBar ] [ text msg ]

errorView message =
  div [ style Styles.errorBar ] [ text message ]

noneView =
  div [ style Styles.noneBar ] [ ]

今までclass属性でスタイル指定していた部分をstyle属性に直すだけ。スタイルはStyle.elmに書いてあるから、使用する側のコードは全く汚れない。今まで通り普通に分離できる。

便利なCSSはそのまま使える

「スタイルをJS」でと聞くと、今までのCSSの知見をすべて捨てて何から何まで自力で座標計算から何からやらなきゃいけないような気がしてくるが、そんなことは全くない。上の例に示したように、positionz-indexpointer-eventsのようなほぼすべてのCSSの便利プロパティは健在だ。Virtual DOMを使っていてもtransitionが動作しないなどのバグも特に無かった。

スタイルの合成が可能

これは「CSS in JS」のそもそもの動機なので言うまでもないのだが、使いまわせるスタイルは変数にしておくことで簡単に合成できる。上の例でいうと、successBarmessageBarに成功時特有のスタイル(緑色とか)を足し合わせることで定義できる。

OOCSS(Bootstrapのような)でも合成は出来るのだが、必要なclassを全て書き連ねる必要があるのでHTMLがどんどん汚れていってしまう(class="message-bar message-bar-success")。スタイル定義側で合成すればHTMLはクリーンに保てるし、「HTMLにはセマンティクスを記述してスタイルはCSS側で~」という昔ながらの価値を取り戻すことが出来る。

スタイルを関心別にまとめて管理

変数を使うことで、よりよいスタイルの管理も可能だ。例えば「色は全部まとめて一つのファイルに定義しておく」とか「z-indexは一か所にまとめて管理する」のようなニーズにも自由自在に対応できる。

動的な値を使った計算が可能

CSS(Sassなどのプリプロセッサを含む)にどうしても出来ないことの一つに「実行時に動的に変化する値を参照」というものがある。「画面サイズ」がその典型例で、「メインコンテンツの高さは画面全体の高さからヘッダの高さを引いた値」とか「コンテクストメニューが画面からはみ出ないように位置を調整する」みたいなことが出来ない。仕方がないので今までそういうロジックは例外的にJavaScript内に書いていたのだが、「CSS in JS」では動的に引数を与えることが出来る。

以下は引数を用いたスタイル定義の例(Elm)。

-- 画面の高さを引数に取る
mainView windowHeight =
  ( flex ++
      [ ("height", px (windowHeight - headerHeight))
      , ("position", "relative")
      ]
  )

数値だけではなく、フラグ的なものを突っ込むこともできる。タブなどを選択したときのactive状態などが典型例。実際のコードからの抜粋。

-- 数値(インデックス)と真偽値の2つを引数に取る
subViewTab index active =
    [ ("position", "absolute")
    , ("top", px (10 + index * 130))
    , ("left", "-30px")
    , ("width", "30px")
    , ("height", "120px")
    , ("padding-left", "6px")
    , ("line-height", "135px")
    , ("background-color", "#eee")
    , ("z-index", zIndex.subView)
    , ("cursor", "pointer")
    , ("border-radius", "8px 0 0 8px")
    , ("box-shadow", if active then "" else "inset -4px 0 4px rgba(0,0,0,0.03)")
    , ("box-sizing", "border-box")
    ]

今まではそれが出来なかったのでHTMLやJavaScriptに書かれていたが、本来このような記述は普通にCSSに書かれるべきもののはずだ。もちろん何でもかんでもロジックを突っ込んでしまうと責務をオーバーしてしまうので、そこは節度を守ってやる。

CSSを使った方が良い場面もある

CSS in JS」だからと言って、別にCSSを使うことを禁止する理由もないので必要ならCSSを使う

まず、インラインスタイルで難しいこととして最初に思い浮かぶのが:hover。流石にこれはCSSでやらないと面倒だろう。JavaScriptでも出来なくはないが、enter/leaveイベントを自力でハンドルする必要があるし、じゃあそのホバーしている状態がロジックとして必要になるケースがあるかと言ったら、まぁない。だから:hoverは諦めて普通にclass属性を使うことにした。

次に、reset系CSS。これもCSSでやっておかないと面倒なことになる。やらない場合「全てのスタイルはこのベースのスタイルを継承してください」のような厄介なルールを作ることになる。嫌な予感しかしないので最初から避けるのが良さそうだ。

弱点に見える部分もほぼ代替手段がある

その他、気付いた点。

  • animationはうまく出来ない。JavaScriptでやる。※未検証
  • ::before/::after疑似要素は普通にJavaScript/HTMLに書けば問題なし。
  • メディアクエリはおそらくフラグで出来る。※未検証
  • ブラウザのツールで同じスタイルを一括変更してプレビューできない。例えば、<li>要素の最初の一つをブラウザ上で変更しても残りには反映されない。JavaScriptのスタイル定義を書き直して画面をリフレッシュする必要がある。
  • BootstrapのようなCSSフレームワークが使えない。それの問題を解決したいので仕方がない。「CSS in JS」用の良いフレームワークが出てくるのを待つか、自分で作る。

自分が納得のできるメリットを見つける

CSSの弱点」とかで調べるとモジュラリティがどうとかいう話がよく出てくるのが、ぶっちゃけよく分からなかった。具体的にどういうコードだとモジュラリティが上がった状態で何が解決するのかよく分からない。

だから「z-indexがー」でも「BEMがめんどくさい」でも「書いてて快感物質が出る」でもなんでもいいけど、理屈はさておき自分の実感できる理由で採用すれば良いんじゃないかなと。逆に実感できないなら主張してもあんまり意味がない。たとえば名前空間の問題も理屈上そうなんだろうけど、自分がそれで困るプロジェクトに関わってなくてよく分からなかったので、ここには書かなかった。あと、CSSではなくてJSのモジュールとして書けるのでエコシステム的にも良いと思っていて、本当はそこも言いたいんだけど試す機会がなかったので誰か知見を持っていれば聞きたい。

まとめ

最初に感じた「スタイルが分離できなくなるんじゃないか」という懸念とは真逆で、むしろ今までJSでゴリゴリ書いていたスタイルに関するロジックが、ちゃんとあるべき場所に収まった

レッツトライ。

自分自身の反省でもあるんだけど、固定観念は良くないと思いました。

以上。

関数型言語Elmでオブジェクト指向する

f:id:jinjor:20160420163043p:plain

(4/23 追記:はてブのコメントで指摘をいただいた箇所を直しました。ありがとうございます!)

最近またElmを触り始めているので小ネタを書きます。

このエントリで主張したいこと。

オブジェクト指向とは?

オブジェクト指向と言うと色んな意味を含んでいて、解釈の違いで論争になるのでまずは整理します。だいたい特徴として挙げられるのは以下でしょう。

どのオブジェクト指向に馴染んでいるかは開発者のバックグラウンドによって異なると思いますが、私はオブジェクト指向と言ったら圧倒的に「カプセル化」であって「役割分担」です。というわけで、以降はカプセル化の話です関数型言語オブジェクト指向の代表としては、ElmとJavaScriptを例として取り上げます。Elmに馴染みのない方でも雰囲気は掴めると思います。

関数型言語は構造体に状態を持たないのでは?

カプセル化と言えば、オブジェクトが所持している「状態」を管理するためのもので、関数型言語とは相容れないような気がします。しかし変数(メモリ)の管理方法が違うだけでやっていることは同じです。 例えばUserという構造体の中のnameという値を変更したい場合、JavaScriptでは構造体の中身を書き換えますが、Elmではname以外はそのままに、nameだけ変更した新しい構造体を返します。

// JavaScript
user.name = 'John';// userの中身を書き換える
-- Elm
user' = { user | name = 'John' } -- 新しいuserを返す

新しい構造体を生成していますが、意味的にはそのUserの状態と考えても問題ないはずです。

カプセル化は何が嬉しいんだっけ?

状態へのアクセス方法・変更方法の制限です。構造がむき出しになっているとどんな操作でも可能になってしまいますが、適切なAPIを提供することでそれ以外の操作が起こる可能性をなくしてくれます。

以下は、ゲームで敵にダメージを与える例です。

// JavaScript
enemy.hit(9999);
var dead = enemy.isDead();
-- Elm
enemy' = Enemy.hit 9999 enemy
dead = Enemy.isDead enemy'

このとき、enemyが内部でどんな変数を持っているかを利用者は知らずに済みます。例えばenemyが内部変数hpを持っていたとしても、hpがゼロかマイナスかを気にする必要はありません。isDeadメソッド(関数)を呼び出せば真偽値が返ってきます。また、もしhpがおかしな値になっていたとしても今のところhpを変更できるのはhitメソッド(関数)だけなので、原因をすぐに追及することができます。

重要なことは、カプセル化によってこのような設計を可能にする必要性に関しては、MutableでもImmutableでも関係ないという事です。Immutableにすれば参照を共有することによる予期せぬ副作用を防いでくれますが、どの道このようなカプセル化のアプローチは必要になってきます。

Elmでオブジェクトを作る

先ほどの例をElmでさくっと実装してみます。

module Enemy(Enemy, init, hit, isDead) where

type alias Enemy =
  { hp : Int, mp : Int }

init : Enemy
init =
  { hp = 100, mp = 30 }

hit : Int -> Enemy -> Enemy
hit amount enemy =
  { enemy | hp = enemy.hp - amount }

isDead : Enemy -> Bool
isDead enemy =
  enemy.hp <= 0

ここではなんとなく見慣れたオブジェクト指向と似たコードだ、ということだけで十分です。関数型言語だからと言って肩肘張る必要はありません。同じようにやれば良いのです。

ところで、実はこのEnemy型のhpmpという値には普通にアクセス出来てしまいます。ここからはElmに限ったテクニックですが、次のように書き換えると解決します。

module Enemy(Enemy, init, hit, isDead) where

type Enemy =
  Enemy { hp : Int, mp : Int }

init : Enemy
init =
  Enemy { hp = 100, mp = 30 }

hit : Int -> Enemy -> Enemy
hit amount (Enemy enemy) =
  Enemy { enemy | hp = enemy.hp - amount }

isDead : Enemy -> Bool
isDead (Enemy enemy) =
  enemy.hp <= 0

type Enemy = Enemy { hp : Int, mp : Int }の右辺のEnemyはUnion Type*1のタグです。Maybe型でいうとJustNothingに相当します。そして重要なことは、このEnemyタグはモジュールの外に対して意図的に公開していません。つまり、Maybe型でいうとJustNothingが公開されていないのと同じです。つまりモジュールの外側ではパターンマッチが出来ず、中の値を取り出すことができません{ hp : Int, mp : Int }はプライベートな値なのです。

少しだけコードが煩雑になるので、あまり厳しくなくてよい時は上の方法でやりますが、きちんと管理したい場合は下の方法でより厳しいカプセル化が可能です。

The Elm Architectureにおけるカプセル化

ElmではThe Elm Architecture(以降TEA)と呼ばれる設計でアプリを作るのが一般的になっています。

以下のリンクは、TEAに沿って作られたCounterコンポーネントのコードです。

elm-architecture-tutorial/Counter.elm at master · evancz/elm-architecture-tutorial · GitHub

TEAではコンポーネントから発火したイベント(Action)は必ず一度ツリーの頂点に到達し、アプリケーションの状態を更新します。しかし、コンポーネントから発火したイベントによって影響を受けるのはコンポーネント自身であることが多く、外部にActionの詳細を公開する必要がありません。CounterコンポーネントActionは次のように定義されています。

type Action = Increment | Decrement

このIncrementDecrementという2つのアクションは外部に公開されていません。それぞれCounterコンポーネントview関数で発火し、Counterコンポーネントupdate関数で使われます。

どうモジュール分割するか

TEAでは、アプリケーションの全ての状態を一か所で管理するため、放っておくとどんどんその部分のコードが膨らんでいきます。 そこで、ざっくり方針として

  • 似たフィールドが3つ以上続いたらまとめる

ことにします。例えば、次のようなコードがあったとします。

-- Main.elm

type Action = Select Id | ChangeName String | Ctrl Bool | Shift Bool | Alt Bool

type alias Model = { selected : Id, name : String, ctrl : Bool, shift : Bool, alt : Bool }

update : Action -> Model -> Model
update action model =
  case action of
    Select id -> ...
    ChangeName name -> ...
    Ctrl isDown -> { model | ctrl = isDown }
    Shift isDown -> { model | shift = isDown }
    Alt isDown -> { model | alt = isDown }

この時点で既にActionが5個、Modelのフィールド数も5個で、このまま機能追加していくとパンクすること必至です。しかし、よく見るとなんとなく、ctrl, shift, altという3つのフィールドは一か所にまとめられそうな気がします。そこで、この3つのフィールドと付随するActionをKeys.elmに切り出します。

-- Main.elm

type Action = Select Id | ChangeName String | KeysAction Keys.Action

type alias Model = { selected : Id, name : String, keys : Keys.Model }

update : Action -> Model -> Model
update action model =
  case action of
    Select id -> ...
    ChangeName name -> ...
    KeysAction action -> { model | keys = Keys.update model.keys }
-- Keys.elm

type Action = Ctrl Bool | Shift Bool | Alt Bool

type alias Model = { ctrl : Bool, shift : Bool, alt : Bool }

update : Action -> Model -> Model
update action model =
  case action of
    Ctrl isDown -> { model | ctrl = isDown }
    Shift isDown -> { model | shift = isDown }
    Alt isDown -> { model | alt = isDown }

すっきりしました。キーに関する情報はすべてKeys.elmに押し込めることが出来ています。

機能追加していると、本当にあっという間にコードが膨らんでいってしまうので、モジュール分割は気付いた段階でしておくべきでしょう。Elmは強力な静的型付け言語なので、大規模にリファクタリングしてもほぼバグりません。気付いた時に気軽にリファクタリング出来るのは大きな強みです。

適切な粒度

オブジェクト指向において適切に役割分担するには、経験上、デメテルの法則を意識すると大体上手く行きます。具体的には

  • 2つドットが続くと危ない

を意識します。例えば、ある会社が自社で運用するサービスを開発するとします(コードはJavaScript)。

company.getDev().work();

この例では、会社から開発者を引っ張り出して働かせています。これでは開発物はできるかもしれませんが、サービスが運用されるかはわかりません。すぐに次のコードが必要になります。

company.getDev().work();
company.getOps().work();

このように一連の手続きをセットにする場合、利用者に毎回同じ手続きを踏むように強制するのは困難です。次のようにすべきでしょう。

company.run();

Elmでも同じことができます。もしMainモジュールがあらゆる処理で埋め尽くされていたら、まず切り離すべきは詳細に踏み込みすぎたコードです。

まとめ

ここまで見てきた通り、「データと処理を一緒にする」という発想は関数型言語においても自然と導かれます。また、カプセル化もMutableなオブジェクトの特権ではなく、Immutableなオブジェクトでもモジュールが処理を制約できれば普通にできます。

このエントリの狙いは、オブジェクト指向言語から関数型言語に移行する際のある種の懸念を払拭することです。お役に立てれば幸いです。

*1:またはtagged union, ADT

Markdown形式で書いた記事やレポートをPDF形式で配布する

あまりまとまってる記事がないので書いた。

やりたいこと

Markdownでさらっと書いた記事を社内に共有したい。 しかしMarkdownだと色々問題がある。

  • 標準的なビュアがない
  • PC環境によって見え方が変わる
  • 画像などを含めるためにZIPなどで共有する必要があり読む側が面倒

Gistでもいいが、GistだとprivateにしてもURLがバレたら普通に外から見えるので実質public。

というわけで、PDF1ファイルにまとめたい。 スタイルを含んだHTML1ファイルでも良さそうだが、フォントがOSによって変わったり、Webフォントだとしてもオフライン時に崩れる問題があるので、やはりPDFが理想。

Markdownの下準備

PDFに変換する前にやっておくと良いかもしれないこと。

目次の自動生成

ある程度長い場合は目次が作りたくなる。 Node.js環境ならdoctocというツールが使える。

改ページの制御

PDF出力または印刷時にどうしても改ページしたい場所があれば、Markdown中に次のコードを入れる。

<div style="page-break-before:always"></div>

あるいはclass指定してCSSで制御しても良い。

PandocによるHTMLの出力

Pandocを使うとMarkdownからHTMLやPDFが作れる。 ただし日本語の場合はPDFを作るのにTeXが必要。そのためだけにTeXをインストールしたくないので、HTMLを出力してChromeの印刷機能でPDFを出力することにする。

シンタックスハイライトを有効にする

Pandocのオプションで有効にする言語を指定。複数の場合はカンマ区切り。

--indented-code-classes="haskell,css"

Markdownで次のように言語を指定します。

 ```haskell
 qsort [] = []
 ```

CSSをカスタマイズする

Pandocで何も指定しないとスタイルが微妙だったりするので、CSSを指定するようにする。ちなみにHTMLには埋め込まれない模様。

pandoc article.md -s -o output.html -c style.css

GitHub風にしたい場合はgithub.cssが使える。あとはBootstrapベースで作られたこちらのCSSもなかなか良い。

必要に合わせてfont-familyなどを変更する。以下は追記するCSSの例。

body {
  padding: 10px 10%;
  font-family: 'Noto Sans Japanese', "Hiragino Kaku Gothic Pro", meiryo, "Open Sans", sans-serif;
}
table {
  width: 100% !important;
}
col:first-child {
  width: 2%;
}
img {
  max-width: 100%;
}

画像幅をカスタマイズする

Markdownでは画像の大きさが指定できないので、CSSで一つずつ書く。

img[src="foo.png"] {
  width: 75%;
}

特定のテーブルのスタイルをカスタマイズする

CSSで指定するために本当はテーブルにIDを振りたいが、そういう機能がないので隣接セレクタでなんとかする。

<div id="table-foo"></div>
|名前|値段|
|:--|:--|
|みかん|50円|
|りんご|100円|
#table-foo + table {
  width: 30% !important;
}

Webフォントを使う

Windowsだとフォントが微妙だったりするので、Webフォントを使う。上のCSSで指定している'Noto Sans Japanese'はHTMLに次のコードを加えると使えるようになる。

<style>
  @import url(http://fonts.googleapis.com/earlyaccess/notosansjapanese.css);
</style>

HTMLに直接書くと次の更新時に上書きされてしまうので、HTMLのヘッダにコードを含めるオプションを利用する。

-H import-font.css

ChromeによるPDF出力

Chromeブラウザで印刷画面を出して(Ctrl+P)PDFで保存。他のブラウザは使っていないので知らない。

f:id:jinjor:20160204151733p:plain

以上。あとはこの一連のプロセスをスクリプト化したら快適になりそう。

TOEICのリスニングCDを分割するWebアプリを作った

TOEICのリスニング問題集をやっていて「ムキーッ!」となることありませんか?

私は2つほどあります。1つは「ひとつの問題を繰り返して聞きたいのにファイルが分かれていない」こと、もう1つは「何を言ってるのかさっぱり分からない」ことです。そこで今回、1つめの問題を解決すべく、CD音源を複数の問題別に分割するWebアプリを作りました。

Wave Cutter for TOEIC®Source

f:id:jinjor:20151228012935p:plain

ChromeFirefox、Edgeで動作確認済みですので、ぜひ遊んでみてください。

使い方

  1. MP3ファイルを読ませると自動的に空白を判断して分割します
  2. 自動分割で上手くいかなかったところを手動で調整します
  3. 完了ボタンを押すとZIPファイルが手に入ります

2に関しては、出力予定のファイル名(左側)と波形データの内容(右側)を一致させるゲームだと思うと手っ取り早いです。

主な機能

  • 波形の削除、分割、結合、再生
  • 分割後のファイル名の付け方の指定
  • Undo/Redo
  • 自動保存

技術的な解説

大掛かりなフレームワークに飽きてきたのでミニマルな感じで攻めてみました。

Virtual DOM

Virtual DOM実装としてSnabbdomを使いました。理由は以下です。

  • 軽い: コアが200行程度
  • 簡潔な記法: h('div#foo.bar.baz')のようにNodeがさくさく書ける
  • Hook機能: パッチを充てる前後などに処理を書ける

Hook機能は、canvas要素のようなVirtual DOM的な思想から外れるものを扱うときに便利です。 今回は音声処理という重い処理を扱うので、Modelが変更されていなければrenderXXX()を走らせないということもしています。

それから、requestAnimationFrame()を使ってレンダリングの頻度を抑えています。 これはModelからViewを生成する関数が純粋であることが前提です。

簡易Flux

ライブラリなしで簡単にFluxしました。

Actionを溜めておくことでUndo/Redoへの対応が楽になりました。最初の状態とActionのリストさえ覚えておけば任意の状態を再計算できます。ただし全てのActionを溜めてしまうと、hoverやらtickみたいな頻度の高いActionに汚染されてしまうので最小限に。言い換えると、Undo/Redoを完全にフレームワーク任せにすることはできません。

今回はModel(Store)をObserverにする必要は無いのでカット。というより、ModelからViewへの紐づけをObserverでやるのはBackbone.jsでカオスになった事があって懲りています。

File API

読み込みと書き込みに使用。

Web Audio API

音声処理に必須です。

Web Workers

編集後、MP3に再エンコードする時に画面がフリーズしたので急遽導入。

用途をTOEICに限定するメリット

無駄に汎用的に作りたくなる気持ちを封じることでメリットを出します。

  • 空白時間はおよそ決まっているので、ユーザーがしきい値などを設定する必要がない。
  • 波形の分割ポイントを「空白の最後(次の波形の直前)」に限定できるので、分割ポイントを選択しやすい。
  • ファイル名の付け方のパターンが決め打ちできる。例えば、Part3からは41-43.pm3などの名前が嬉しいと分かっている。

まとめ

最新のWeb技術を使って役立ちそうなものを作ることができました。残タスクは以下です。

  • MP3エンコードの高速化
  • メモリ不足対策
  • CDによる差異を埋めるために分割ロジックを賢くする
  • UIを洗練させる
  • リスニングを克服する

以上。