anti scroll

ブラウザと小説の新しい関係を模索する

Nehan.createPagedElementの紹介

nehan.jsでは、組版エンジンを扱う入り口として、主にPageStreamというものを提供しています。

ただしこれは柔軟に扱える反面、はじめての利用者にとっては直感的じゃなかったかもしれませんでした。

そこで、nehan.jsのversion5.0.3からは、より扱いやすい抽象化としてPagedElementというものが利用できるようになりました。

Nehan.createPagedElementで作成できるのですが、イメージとしてはdocument.createElementに近いものです。

PagedElementはDOM elementを内包しますが、通常のDOMと違って内部に複数ページを持っていて、それらを切り替えられる点が異なります。

PagedElementの作成

Nehan.createPagedElementで作成します。

var paged_element = Nehan.createPagedElement();

スタイルの設定

続いて、ページのサイズをsetStyleでセットします。

setStyleの第1引数は、セレクターです。

"body"がページのルートスタイルになります。

paged_element.setStyle("body", {
  "flow":"tb-rl", // 縦書きモード。横書きなら"lr-tb"にする。
  "width":640,
  "height":480,
  "font-size":16 // fontSize(camel case)とは書けない。
});

cssのプロパティとして、camel caseが使えないことに注意してください(つまりfont-sizefontSizeと設定することはできない)。

なおその他の要素のスタイルも、同じようにセットできます。

paged_element.setStyle(".my-header h1", {
  "font-size":"3em"
});

ページ内容をセット

ページの内容はsetContentでセットします。

setContentした直後に組版が始まるので、上述のsetStyleを先に済ませておく必要があることに注意してください。

paged_element.setContent("<h1>hello, nehan.js</h1>");

組版結果のDOMを取得

paged_elementによって組版されるDOMは、getElementで取得できます。

var element = paged_element.getElement();

// 画面上のどこかにセット
document.getElementById("result").appendChild(element);

次ページ、前ページの描画

次ページの描画はsetNextPageで、前ページの描画はsetPrevPageです。

// 次ページボタンをクリック
document.getElementById("next-button").onclick = function(){
  paged_element.setNextPage();
};

// 前ページボタンをクリック
document.getElementById("prev-button").onclick = function(){
  paged_element.setPrevPage();
};

組版の非同期コールバック

ページを組版している最中の状況(パーセントみたいなもの)が欲しい場合は、setContentの第2引数にコールバックを指定します。

onProgressonCompleteが有効です。

paged_element.setContent("適当な内容", {
  onProgress : function(tree){
     console.log("%d page(%d percent) is done", tree.pageNo, tree.percent);
   },
  onComplete : function(time){
     console.log("finish! %f time", time);
  }
});

ページ数、アウトラインの取得

ページ数やアウトラインは、上記のonCompleteのタイミングで確定します。

なので次のように取得すると良いでしょう。

paged_element.setContent("<h1>適当な内容</h1>", {
  onComplete : function(time){
     console.log("page_count = %d", paged_element.getPageCount());
     console.log("outline element = %o", paged_element.engine.createOutlineElement());
  }
});

createOutlineElementについては、paged_element.engineという内部エンジンを経由して呼んでいる点に注意してください。

またcreateOutlineElementは、引数にコールバックオブジェクトを与えることができます。

それによって、アウトラインをクリックした時の挙動なども設定できるのですが、詳しくはnehan.jsのsection-tree-converterを参照してください。

jekyllで縦書き横書きの本が簡単に投稿できるテーマ

jekyll-nehanというものを作りました( デモを見る)。

Jekyllという静的サイトジェネレータのテーマです。

このテーマを使うと、nehan.jsを使った縦書き横書きのページ送りエントリが簡単に投稿できます。

スクリーンショット

見た目はこんな感じになります。

f:id:convertical:20140624173156p:plain

f:id:convertical:20140624173203p:plain

レスポンシブ対応

スマホ等から見ると、こんな感じに。

f:id:convertical:20140625122844p:plain

使い方

投稿記事のヘッダにbook_typeを指定するだけです。

---
layout: post
title: your awesome book
date: 2014-06-24
book_type: vert
---

book_typeには、vert(縦書き)とhori(横書き)が指定できます。

キーボードショートカット

  • j : 次ページ
  • k : 前ページ
  • left: 次ページ(vertの時), 前ページ(horiの時)
  • right: 前ページ(vertの時), 次ページ(horiの時)

reactive programming / getterは共変、setterは反変

reactive programmingについての動画(語り手がハイテンション)を見つけました。

http://channel9.msdn.com/Events/Lang-NEXT/Lang-NEXT-2014/Keynote-Duality

まだ前半しかみてないのですが、途中getterとsetterを「共変(covarience)」と「反変(contravarience)」という概念を使って説明している箇所があって、あんまりピンと来なかったので、少し落ち着いて考えてみました。

というわけで以下は備忘録です。

サブタイプ

動画の人は「コーラはソーダのサブタイプ」って説明してました。

ソーダ型はコーラ型を含むので、ソーダ型で記述できた式は、ソーダの部分をコーラに置き換えても動作するはずです。

で、こういう状況を

ソーダ型 >= コーラ型

なんて書くことにします。

共変と反変

例えば型をもとに新しい型を作る型構築ルールRがあったとします。

で、これをソーダ型に適用したR(ソーダ型)と、コーラ型に適用したR(コーラ型)が、

R(ソーダ型)>= R(コーラ型)

という風に、元の関係を維持しているとき(つまりR(ソーダ型)の部分を、R(コーラ型)で置き換えることができるような状況になっているとき)、Rを「共変」と言います。

逆にR(コーラ型)の部分が、R(ソーダ型)で置き換え可能な状況になっている場合は「反変」と言います。

で、動画(の前半)で言ってることは、元のサブタイピングの関係に対して、getterの型構築子は共変だけど、setterの型構築子は反変になるから気をつけてね、ってことだと思います。

getterは共変

例えば、NumのサブタイプをIntとします。

で、Numは他にもFloatとかもサブタイプに含むものとします。

Num >= Int
Num >= Float

続いて、getterを作る型構築ルールを次のように定義します。

getter(A) = () -> A

つまり、getter(A)は型Aから「何かしたらAが返る」という関数型を生み出すものとします。これをNum/Intに当てはめると

  • getter(Num):() -> Num
  • getter(Int): () -> Int

となります。

さて、プログラムの中に、getter(Num)が求められている処理があったとして、それをgetter(Int)に置き換えることはできるでしょうか。

これは問題ありません。

なぜならgetter(Num)を処理できる式は、getter(Num)の返り値となるNum型を処理できるのであり、そこがInt型を返すgetter(Int)に置き換わったとしても、問題なく処理できるはずだからです。

つまり getter(Num)な部分は、getter(Int)で置き換え可能なので、

getter(Num) >= getter(Int)

となります。

これは元のサブタイピングの関係(Num >= Int)と方向が一緒なので「それぞれの型を返す関数を作る」getter型構築子は共変(covarient)である、と言えます。

setterは反変

次にsetter型をつくる型構築ルールを次のように定義します。

setter(A) = A -> ()

型Aを受け取って何かをする、という型です。

つまり

  • setter(Num): Num -> ()
  • setter(Int): Int -> ()

このとき、setter(Num)が求められている箇所を、setter(Int)に置き換えることは出来るでしょうか。

これは出来ません。

なぜなら、Int型だけじゃなくFloat型も受け取るであろうsetter(Num)を前提に書かれた箇所を、Int型しか扱えないsetter(Int)で置き換えることはできないからです。

しかし、その逆はできるのです。

つまり、setter(Int)を受け取る部分を、setter(Num)に置き換えることは出来ます。

なぜなら、setter(Int)が受け取る型のすべて(といってもIntだけですが)を、setter(Num)は受理できるからです。

よって、型変換後の両者の対応関係は、

setter(Int) >= setter(Num)

となり、これは元のサブタイピングの関係性(Num >= Int)と方向が逆なので、「それぞれの型を受け取って何かする関数を作る」というsetter型構築子は反変(contravarient)である、と言えます。

(こんな感じの理解でいいのかな?)

参考:反変性と共変性(計算機科学)

nehan.jsの正規表現セレクタ

nehan.jsのversion5から、セレクターのエレメント名の部分が正規表現で記述できるようになっています。

サンプル

例えば、ヘッダー要素なら全てに適用したいスタイルがある時は、こんな風に書けます。

// すべてのh1〜h6
Nehan.setStyle("/h[1-6]/", {
   "font-weight":"bold"
});

// .header以下のh1〜h6
Nehan.setStyle(".header /h[1-6]/", {
   "color":"#FF0000"
});

h1h6のそれぞれにスタイルを書くよりは楽ですよね。

htmlタグにはヘッダータグのように、名前の中に意味を内包するタグもあるので、こうやって名前を正規表現でグルーピングできるのは便利だと思います。

特に自分専用のカスタムタグを作るときなんかは、重宝するかもしれません。

注意

正規表現で指定できるのは、エレメント名の部分だけです。

次のように、クラス名に該当する部分を正規表現にすることは出来ません。

// こういう指定は出来ない。
Nehan.setStyle("div/¥.(?:b|strong)/", {
  "font-weight":"bold"
});

// こういう指定なら出来る。
Nehan.setStyle("/(?:b|strong)/", {
  "font-weight":"bold"
});

nehan.jsで閉じタグの不要なタグ(single tag)を定義する

前回発表したアイコンの埋め込み特殊タグですが、こういうタグをnehan.jsでどうやって定義するのかを紹介したいと思います。

おさらいですが、先月に発表したfaタグはこんな風に宣言します。

ハートのアイコンは、<fa name="heart">

imgタグとかhrタグみたいに、閉じタグがありませんよね。

実はこういう閉じタグが不要なタグを定義する場合、事前にnehan.jsのパーサーに対し、その旨を教える必要があるのです。

じゃないと、通常のタグと同様に「閉じタグがあるもの」としてパースしてしまい、結果としてfaタグに続く全てのコンテンツがfaタグの「中身」として登録されてしまいます。

なので、こんな風に指定して、事前にfaタグが閉じタグのないタグであることをnehan.jsに教えます。

Nehan.addSingleTagByName("fa");

後は、通常のスタイル設定と一緒で、このタグの振る舞いを定義します。

Nehan.setStyle("fa", {
  display:"inline",
  width:"1em",
  height:"1em",
  onload:function(ctx){
    var markup = ctx.getMarkup();
    var icon_name = markup.getAttr("name");
    markup.setContent("<i class='fa fa-" + icon_name + "'></i>"); 
    markup.setAttr("pasted", true);
  }
});

やっていることは、

  • onloadでマークアップの読み込みをフック
  • markupの中身を通常のfont-awesomeの書式(iタグを使った書式)に書き換える
  • markupの中身をnehan.jsに組版させず、そのまま貼付けさせるために、pasted属性をセットする

というものです(縦書き文庫では、実はもう少し色々とやっていますが、ここでは簡単の為に最低限の処理だけ書きました)。

最後のpasted属性に付いては補足が必要です。

実はこの属性、まだ公式のドキュメントに書いていないのです。

なぜかというと、運用の仕方とか命名について「いいのかな」ってモヤモヤしているからです。

もしかしたら、そのうち仕様とか命名とか変わるかもしれません。ご注意ください。

Font Awesomeのアイコンが簡単に埋め込めるようになりました

Font Awesomeのアイコンを簡単に埋め込めるタグfaを用意しました。

name属性にアイコン名を指定します。閉じタグは不要です。

例えばこんな感じで書くと…

ユーザー:<fa name="user">
スター:<fa name="star">
ハート:<fa name="heart">
ハート(赤):<fa name="heart" style="color:red">
ローディング:<fa name="spin spinner">
回転する歯車:<fa name="spin cog">

こんな感じに表示されます。

f:id:convertical:20140523104932p:plain

詳しくはマークアップヘルプのアイコンフォントを入れるを参照してください。

HTMLの閉じタグを省略する記法について、パフォーマンスの検証と考察

nehan readerで長文エントリーを読むときに、少し前まで正常にパースできていたページが、ある日から突然レイアウト崩れを起こしてしまったりすることがあります。

で、調べてみたら、閉じタグの省略を使ったマークアップに変更されたことが原因のようでした。

この閉じタグの省略ルール、ルールが割と複雑で、パーサーが作りにくいので好きではないのですが、書き手にとっては読みやすいし、字数が減るので帯域は削減されるのはもちろん、ブラウザの表示パフォーマンスも(それなりには)速くなるはずです(理由は後述)。

検証コード

ただ、どのぐらい高速になるのかが少し気になったので検証してみました。

閉じタグを省略しないケースの検証コードはこんな感じで…

$(function(){
  var parts = [], node = "<li>hoge</li>";
  for(var i = 0; i < 10000; i++){
    parts.push(node);
  }
  var text = parts.join("\n");
  console.time("no abbr");
  document.getElementById("dst").innerHTML = text;
});

省略した場合はこういう感じのコードで検証しました。

$(function(){
  var parts = [], node = "<li>hoge12345";
  for(var i = 0; i < 10000; i++){
    parts.push(node);
  }
  var text = parts.join("\n");
  console.time("with abbr");
  document.getElementById("dst").innerHTML = text;
});

省略したバージョンでは、入力データのサイズを同じにする為、12345という文字をリストの文字列に追加しています。

そして、いずれもbody.onloadのタイミングでタイマーを止めています。

 <!-- 省略なし -->
<body onload="console.timeEnd('no abbr')">

<!-- 省略あり -->
<body onload="console.timeEnd('with abbr')">

検証結果

結果はこんな感じです。

time(msec)
閉じタグなし 224
閉じタグあり 2159

閉じタグを省略したケースが、10倍ほど高速という結果に。

速度差が出る理由の考察

閉じタグがある場合は、再帰的な入れ子を考慮して、対応する正しい閉じタグの位置を検索するロジックが入るから遅くなるのではないかと。

例えば、

<div>aaa<div>bbb</div>ccc</div>

では、aaaの位置から閉じタグを探すと、最初にbbbの右にある/divが見つかりますが、そこに至る前に入れ子divが開かれているので、スキップしてcccの位置から再び/divを探さなければなりません。

一方で閉じタグの省略と見なせるパターンでは、こういう再検索は不要で、もうそこがタグの終了位置だと確定して次のパースに進むことが出来るので速いのだと思います。

トレードオフ

グーグル検索のトップページみたいに、とんでもないアクセスがあるけど、ページの内容そのものにはドキュメント的な意味が少ない場合なら、帯域を削減するマークアップは意味がありそうです。

けれども、ページにきちんとした内容(文章)がある場合は、マークアップのルールが厳密な方がパーサーは作りやすいので、クライアントがパースしやすいマークアップで配信した方が、ドキュメントの利用価値は上がるような気がします。

nehan.jsで始めるfunctional stylesheet入門

いつも思うんですけど、

  • CSSセレクタのパスを書いてスタイルを設定する
  • 同じものをjavascriptで取得してイベントを設定する

って二度手間じゃないですか?

どっちもDOMノードを指定して、何かの値を設定するってことですよね。

設定するものがスタイルかイベントかってだけの違いです。

なのに、CSSの作法とjsの作法をそれぞれ別々に覚えて設定するっていうのは、設定がそれぞれ別のファイルに散らばってしまってわかりにくいし、なによりマークアップの変化に弱そうです。

なので一緒に書けたら楽だなあ、などと僕は思うのですが、nehan.jsではまさにそういうことができます。

スタイルとイベントを同時に設定してみる

例えばこんな風に設定できます。

Nehan.setStyles({
  ".nehan-fade-in":{
    "color":"green",
    "oncreate":function(dom){
      $(dom).fadeTo("slow", 1.0);            
    }
  }
});

ポイントとなるのは、oncreateなるコールバックです。

.nehan-fade-inで指定されているノードが実際に作成されるタイミングをoncreateでフックし、フェードインの処理を追加しています。

ちなみにクラス名やId名を「nehan-」で始めるというのはnehan.jsの仕様です。 nehan.jsの外の世界で組版されるものとスタイルを衝突させないための仕様です。

関数でスタイルを分岐

次に、functional stylesheetらしく、マークアップ名で色を分岐してみましょうか。

Nehan.setStyles({
  ".nehan-fade-in":{
    "color":function(ctx){
      var markup = ctx.getMarkup();
      switch(markup.getName()){
      case "h1": case "h2" case "h3": return "red";
      case "h4": case "h5" case "h6": return "blue";
      default: return "green";
      }
    },
    "oncreate":function(dom){
      $(dom).fadeTo("slow", 1.0);            
    }
  }
});

cssプロパティのcolorに指定されている値が関数になっていることに注意してください。

関数を指定した場合は、returnで返したい値を出力します。

独自の文法を作ってみよう!

nehan.jsにはoncreateの他にonloadというコールバックも用意されています。

onloadは「そのセレクタに該当するスタイルを全て読み終わり、これから組版計算を開始する直前」をフックするコールバックです。

言葉にすると複雑なようですが、ようするに具体的な組版計算が開始される前に、マークアップの内容やらCSSの値を書き換えてしまうことで、別の組版結果を導くためのものです。

何の役に立つかというと、例えば独自のマークアップ文法を定義するときに便利です。

TIPというタグを作ってみる

ここでは、クリックするとマークアップで囲まれた内容がポップアップするタグtipを考えてみます。

例えばこんなマークアップをしたら、

<tip title='nehan.js'>名前の由来は「ネイティブ組版」</tip>

その結果として

  • 画面にはtipタグの中身ではなく、title属性に設定された文字列を表示させたい
  • その文字列をクリックしたら、中身の説明文がポップアップするようにしたい

とします。

これを実現するには、

  • tipタグの組版が始まる直前をフック
  • コンテンツの内容をdatasetに退避
  • タイトル属性の内容をコンテンツに上書き
  • oncreateにてtipタグがつくるDOM要素を受け取り、onclickで退避させておいた元コンテンツをポップアップさせる処理を書く

みたいにすれば良さそうです。

実際に実装してみましょう。

Nehan.setStyles({
  "tip":{
    "display":"inline",
    // このタグの組版を開始する直前をフック
    "onload":function(ctx){
      var markup = ctx.getMarkup();
      var title = markup.getAttr("title", "no title"); // title属性を取得。ない時は"no title"が返る
      var content = markup.getContent();
      markup.setContent(title); // 中身をタイトル属性の値に置き換える
      markup.setData("content", content); // contntをdata属性(dataset)に保存しておく
    },
    "oncreate":function(dom){
      $(dom).click(function(){
        alert($(this).data("content")); // onclickで、保存しておいたdata属性をalert
      });
    }    
  }
});

上記は最低限の処理しか書いていませんが、例えば「title属性が入っていない時は無効だから何もしない」みたいな処理を加えても良いと思います。

こうやって好きなように自分のローカルな組版文法を定義し、その振る舞いをプログラマブルに制御できる、というのは通常のウェブ開発では体験できないことだと思いますし、なにより楽しいです。

結論

やはりスタイルも関数で書けると、制作者の意図が伝わりやすい気がします。

これらのスタイル指定を一つのファイルにまとめておけば、自分の作った特殊なタグ機能を他者にプラグインとして公開することも容易です。

面白い機能が作れた人は、是非教えてください。