購読中です 読者をやめる 読者になる 読者になる

nehan.jsのデモページをReact/Fluxで作ってみた

必然性は全くなかったのですが、Fluxアーキテクチャで組んでみたかったので試してみました。

http://tb.antiscroll.com/static/nehan-demo/

わかったこと

facebook/flux付属のflux-todomvcを見ながら書いただけなのですが、以下はちょっとした躓きポイントでした。

1. Dispatchに登録したイベントハンドラの中でアクションを作ると「dispatchの途中でdispatchできないよ」と怒られる。

入れ子にせず、個々のアクションにして、外側のレイヤで個々に呼べということでしょう。

2. textareaの内容をタグで囲った場合は再描画の対象にならない

つまり

<textarea>{this.props.code}</textarea>

などとしても再描画されませんが、

<textarea value={this.props.code} />

とすれば反映されるようです。

参考URL

2005年にアジア〜中東〜北アフリカ〜ヨーロッパを旅行したロバ中山さんの旅行記がすごい

2005年、つまり今からちょうど10年ぐらい前、アジア〜中東〜アフリカ〜ヨーロッパを旅行したロバ中山さんの旅行記がすごく面白いです。

ロバ中山の旅日記 トップページ

今は大変な状況になってしまったシリアやイエメンにも行ってますし、イラン、ヨルダン、イスラエルにも行ってますね。

面白くて二度読みしている最中なのですが、現代の中東情勢を伺い知るには、もってこいの内容じゃないでしょうか。

ただし、そのままだと読みづらい気がしたので、自分は NehanReader で次のような設定をして読みました。

  1. 「涅」ボタンを右クリックして「オプション」
  2. 「table of selectors to main article」の欄に、以下の一行を追加。
www.sakaguti.org, td[width="491"], table>tbody>tr>td

こうして設定した後、NehanReader で読むと、こんな感じで読めます。

f:id:convertical:20150424202024p:plain

非常に長い日記ですが、最初から読む必要はなく、興味がある国だけ読んでも面白いと思います。

それにしてもイエメンのヤヒヤさんやアミンさんは無事なのだろうか…と思ってしまいますね。

Nehan Readerアップデート。サイトごとに変換対象を設定できるようになりました

Nehan Reader ver0.9.63から、サイトごとに変換対象を設定できるようになりました。

chrome.google.com

設定の仕方

「涅」のボタンを右クリック→「オプション」と進みます。

f:id:convertical:20150419133103p:plain

「table of selectors to main article」の欄に

[siteのURL], [selector]

を改行で区切って記述し「Save」ボタンで保存します。

サンプル設定

www3.nhk.or.jp, #news
toyokeizai.net/articles, #article-body
www.asahi.com/articles, #Main
togetter.com, .contents_main
ncode.syosetu.com, #novel_contents
lifehacker.jp, article
gigazine.net, #maincol
blog.livedoor.jp, .article-outer
news.yahoo.co.jp, #main
headlines.yahoo.co.jp, #main
hatenablog.com, #main
hatenablog.jp, #main
hateblo.jp, #main
hatenadiary.com, #main
hatenadiary.jp, #main
anond.hatelabo.jp, .day
www.huffingtonpost.jp, article.entry
wired.jp, .article_maincontents

例えば上の内容をコピペするだけで、ヤフーニュースを始めとするニュース記事や、小説家になろうの小説、はてなブログの記事などが読みやすくなります。

また変換するのがメインの文章だけになるので、高速にもなります。

css framework上の全てのセレクタに特定のprefixを付ける

chrome拡張とかでも、普段使っているcssフレームワークが使いたくなることがあります。

しかし大抵のcssフレームワークは、グローバルな名前空間でスタイルを宣言しています。そのまま導入すると、拡張機能CSSが読み込まれてしまった結果、訪問したサイトの本来のスタイルを崩してしまうでしょう。

もちろん拡張機能を特定のサイトでだけ動くようなポリシーにすれば防げます。しかし、NehanReaderのように、全てのサイトで使えるようにしている拡張機能も、たくさんあると思います。

そういう場合、フレームワークで宣言されている全てのセレクタに、強引に何かのプレフィックスを足してしまえば良いわけですが、手動でやるのは流石にキツイ、というか無理です。

だから「なんか良いツールないかなあ」と探して見つけたのがrework-mutate-selectorsです。

github.com

インストール

今回はgulpを使いたかったので、gulp-reworkも一緒に導入しました。

npm install rework
npm install rework-mutate-selectors
npm install gulp-rework

gulpやgulp-renameがない人は、先にインストールしておいて下さい。

使ってみる

var gulp = require("gulp");
var rework = require("gulp-rework");
var selectors = require("rework-mutate-selectors");
var rename = require("gulp-rename");

gulp.task("default", function(){
  return gulp.src("framework.css")
    .pipe(rework(selectors.prefix(".my-module"))) // prefixに".my-module"を追加
    .pipe(rename("my-module.framework.css"))
    .pipe(gulp.dest("."));
});

こうすると例えば

/* framework.css */
div a{ color:red }

が、

/* my-module.framework.css */
.my-module div a{ color:red }

と出力されます。

結論

一瞬、自分で作ろうかとも考えましたが、諦めずに探して良かったとです。

nehan.jsで拡張タグを作る

拡張タグを作るサンプルとして、次のようなタグを作ってみます。

<circular>
  <div>1時ですよ!</div>
  <div>2時ですよ!</div>
(省略)
  <div>11時ですよ!</div>
  <div>12時ですよ!</div>
</circular>

で、例えばこのタグを表示した時刻が1時だったら、次のようになるものとします。

f:id:convertical:20150408134943p:plain

動作デモ

実際に動作させてみた結果は、こんな感じになります。

デモを表示する、を押すと文字がくるっと回転しながら表示され、現在時刻だけが赤くなります。

上手く動作しない場合はページをリロードしてみてください。

基本方針

  1. Nehanのグローバルスタイルにてcircularタグをブロックタグとして登録
  2. circularの子供エレメントの中で、現在時刻に該当する子の文字色を赤に
  3. それぞれの子を円の中心に移動させてから、時刻数に応じて回転させる

すべてのソースは以下のリポジトリに登録しておきました。

github.com

なので、この記事ではポイントだけを解説します。

タグの登録

Nehan.setStyleを使うと、グローバルタグを登録できます。

組版エンジンごとに登録することもできるのですが、今回は解説しません。

circularをブロックタグとして登録したいので、次のようにしてみます。

Nehan.setStyle("circular", {
  display:"block",
  background:"wheat",
  measure:"280px",
  extent:"280px",
  "border-radius":"280px",
  margin:{after:"2em"},
  // 円の直径(280px)を表示する余白がなければ改ページ
  onload:function(ctx){
    var items = [];
    var rest_extent = ctx.getRestExtent();
    var extent = parseInt(ctx.getMarkup().getAttr("extent", "280px"), 10);
    if(rest_extent < extent){
      ctx.setCssAttr("break-before", "always");
    }
  }
});

上のonloadは、circularセレクタの読み出し完了をフックする関数です。

ここで「十分な余白があるかどうか」をチェックする処理が埋め込まれています。

なぜなら、このタグを表示するには、12個ある子供のdivがすべて表示されないといけません。

10時までしか表示されなかったら時計になりませんので。

というわけで、十分な余白がなければ、スタイルに改ページを追加する、という処理がされています。

時刻を記述した行の処理

各時刻(「〜時ですよ!」の行)を指すセレクタcircular divです。

Nehan.setStyle("child div", {
  "line-height":"1em",
  color:function(ctx){
    var child_index = ctx.getChildIndex();
    var cur_hour = new Date().getHours() % 12;
    return ((child_index + 1) % 12 === cur_hour)? "red" : "black";
  }
});

colorのところでは、各子供が現在時刻に該当する時刻を表す行なら赤を返すような「関数値」が設定されています。

各時刻を回転

あとは、各行が時計上で位置する場所へと回転させるだけです。

各行を親のボックスサイズのブロックレベルサイズの半分(*1)だけ移動し、そこからそれぞれ30度(=360/12)ずつずらして表示させたらよさそうです。

*1 - 正確に言うと親ブロックのブロックサイズの半分から、さらに行そのものの高さの半分を引きます。

Nehan.setStyle("child div", {
  onblock:function(ctx){
    var parent_style = ctx.getParentStyleContext();
    var is_vert = ctx.isTextVertical();
    var child_index = ctx.getChildIndex();
    var child_count = parent_style.getChildCount();
    var line_height = ctx.getStyleContext().getFontSize(); // line-height:"1em"
    var parent_extent = ctx.getParentBox().getContentExtent();
    var trans_extent = Math.floor((parent_extent - line_height) / 2);
    var unit_degree = Math.floor(360 / child_count);
    var rotate_degree = child_index * unit_degree + (is_vert? 30 : 120);
    var $dom = $(ctx.dom);
    var translate = is_vert? {translateX:trans_extent + "px"} : {translateY:trans_extent + "px"};
    var rotate = {
       opacity:1,
       rotateZ:rotate_degree + "deg"
    };
    $dom
    .css("position", "absolute")
    .css("opacity", 0)
    .velocity(translate)
    .velocity(rotate);
  }  
});

ちなみに、回転処理をアニメーションさせたかったので、その部分についてはvelocity.jsを利用しました。

onblockについては説明が必要でしょう。

circle divセレクタに対するonblockは、このセレクタを表すエレメントがブロックレベルとしてDOM化されたタイミングをフックしています。

どういうことかというと、実はブロックレベルの下には、匿名ラインボックスという更に下のレベルのDOM化があり、そちらはonlineとして読み出されるのですが、それと区別しているわけです。

例えば<p>hoge</p>というマークアップがあった場合、pはブロックの中にさらにhogeというテキストを表す匿名ラインボックスを含んでいます。

ちなみにoncreateを呼ぶと、ブロックレベルでもラインレベルでも共通で呼ばれます。

今回は行エレメントがブロックレベルで作成された時だけに興味があったので、onblockを使いました。

まとめ

このサンプルを見ればわかると思いますが、レイアウトエンジンレベルでセレクタ定義が出来ると、セレクタの定義とそのセレクタに対するアクションが同時に書けます。

jQueryなどでは、画面に既に表示されたものに対してセレクタ検索をかけますが、このセレクタの検索はレイアウトエンジンからすれば(スタイルを読み出す際に)既に検索済みなので、そのタイミングでjsのコードを埋め込めるのは効率面からも有利でしょう。

さらにスタイルの設定を読み出す際に、スタイルプロパティの値を関数にできるのも大きな強みです。

このサンプルでもcolorの設定は、new Dateして、現在時刻を見た上で色を決定していますね?

circleタグも、表示前に組版状況を見て、余白がなければ動的に改ページしています。

このようにjsと協調しながら、動的にスタイル設定を切り替えることができるのは非常に面白いと思うのですが、どうでしょうか。

nehan.js 5.1.0系

まだ安定していないのでリリースまではしてないのですが、自分の中で懸案だった幾つかのトラブルが解消したっぽいので、nehan.jsのバージョンを5.0.5から、5.1.0へと上げました。

github.com

ちなみに安定していなくても、縦書き文庫では問答無用で最新版を走らせています。

利用者がバグを見つけてくれることが多いからです…

変更点など

1. 複数ページに渡るフロート処理の改善

5.0系列では(主に横書きの時に)バグがあったのですが、5.1系では縦書き横書き共に複数ページに渡るfloatが正しく最後まで表示されるようになりました。

f:id:convertical:20150407202906p:plain f:id:convertical:20150407202917p:plain f:id:convertical:20150407202926p:plain

2. last-child系の取得ができるように

IE8を考慮したlexerの処理を刷新したことで、lexing中に取れる情報を利用できるようになり、結果としてlast-child系のpseudo-class(last-child/last-of-type/only-child/only-of-type)を利用できるようになりました。

これまでのLexerは、主にIE8でフリーズさせないために、バッファリングしながら処理させていたのですが、これだと同一階層のDOMにおける先のlexing内容が不定になるため、first-child/nth-child/nth-of-typeなどはとれても、末尾からの情報は取得できていませんでした。

3. 複数のInlineジェネレータをまたぐ禁則処理の対応

これまでは複数のInlineElementをまたぐ構造になる時、行末の禁則処理が正しく動作していなかった場合があったのですが、この不具合の大半(残念ながら全てではない…)を改善することが出来ました。

4. 複数ページに渡るテーブルのborder-collapseを改善

ページの途中で途切れた場合にもborderの最後が表示されてしまっていたのですが、これが消えるように(ただしたまに表示されることもある)なりました。

f:id:convertical:20150407203246p:plain f:id:convertical:20150407203251p:plain

5. 表示できないエレメントを適切にスキップ

どうやっても指定されたページの中に表示できないレイアウト(深すぎるテーブルとか)はスキップして続きのレイアウトから表示できるようになりました。

6. 処理速度の改善

一部のロールバックが必要だった処理をロールバック不要な処理に改善し、全体のパーススピードが上がりました。

7. セレクタに新しいコールバック関数を追加

onload/oncreateに加えて、onblock/online/ontextというコールバックが追加されました。

それぞれブロックレベルの作成タイミング、その中の匿名ラインボックスの作成タイミング、更にその中のテキストブロックの作成タイミングをフック出来ます。

この辺のことについては、いずれチュートリアルと共に別エントリで説明する予定です。

Prepared Statementを少し使いやすくするeps(Extended Prepared Statement)

通常のPrepared Statementを少し使いやすくする処理系 eps を作りました。

epsはExtended Prepared Statementの略です。

github.com

簡単に言うと、こんな感じでPrepared Statementを記述したくて作ったものです。

prepare foo(age:int, name:text="no name!") as
select * from people where name={name} and age={age};

ようするに

  • ラベル付き引数を使いたい
  • デフォルト引数を使いたい
  • ついでに型を考慮した呼び出し側のコードが出力したい

わけでした。

使い方

上のSQLtest.sqlというファイルで保存したとして、

eps.exe -input test.sql -format sqlとすると

prepare foo(int, text) as
select * from people where name = $2 and age = $1;

が出力されます。また

eps.exe -input test.sql -format ocamlとすると

let prep_foo = 
  "prepare foo(int, text) as select * from people where age = $1 and name = $2;"

let exec_foo ~age ?(name="no name!") () = 
  Printf.sprintf "execute foo(%d, '%s');" age name

のように、デフォルト引数、ラベル付き引数、型制限が付いたコードが出力されます。

メリット?

  1. 通常のプレースホルダー($1,$2みたいなの)じゃなくて、ラベル名を参照したSQLが書ける => うっかり割り当てを間違えたSQLを作る危険性が減る。
  2. 型付きかつラベル引数のOCaml関数で読み出せる => 呼び出し方を間違える危険性が減る。
  3. ついでにデフォルト引数を宣言できる。

現在ターゲットに指定できる言語はOCamlSQLだけですが、そのうちサポート言語を付け足すかも?

Kefir.jsを使ってnehan.jsなビューアーをReactiveに作る

最近、Reactiveは「Re + Active」、つまり「再びアクティブな値を獲得する」みたいに説明したらいいんじゃなかろうか、なんて思ってます。

それはさておき、表題の件に挑戦してみました。

Kefir.jsを使ったのは、ドキュメントがわかりやすくて綺麗だったからです。

参考までに、完全なソースはGitHubに上げておきました。

tategakibunko/nehan-kefirjs-example · GitHub

ビューアーの要件

  1. NEXTクリックでページを進める。
  2. PREVクリックでページを戻す。
  3. $("#page-count")に、現在の総ページ数を表示する(非同期で増える)。
  4. $("#page-no")に、現在のページ番号を表示する。
  5. ただしページ番号は、総ページ数の値を超えることはできない。

実装方針

  • nehan.jsが非同期で新しいページを出力する度に、現在の総ページ数を出力するストリームpage_count_streamを作る。
  • 現在のページ数とNEXT/PREVクリックのアクションを合成して、現在のページ位置を出力するストリームpage_index_streamをる。
  • page_count_streamを購読し、$("#page-count")を更新する。
  • page_index_streamを購読し、$("#screen")に該当ページを出力しつつ、$("#page-no")にページ番号を出力する。

page_count_streampage_index_streamを宣言的に(ステートを持たないように)表現できたら成功。

ページ数のストリームを作成

nehan.jsが作ったページ数を受け取るストリームをKefir.bus()で作成しておきます。

var page_count_stream = Kefir.bus();

ページ位置のストリームを作成

NEXT/PREVのクリックをそれぞれ+1-1を出力するストリームにし、それらを一つに束ねたものをclick_value_streamとして定義します。

var next_click_stream = $("#next").asKefirStream("click");
var prev_click_stream = $("#prev").asKefirStream("click");
var click_value_stream = Kefir.merge([
  next_click_stream.mapTo(1), 
  prev_click_stream.mapTo(-1)
]);

ここからは少し面倒です。

こういうnext/prevでカウントするサンプルって、大抵はストリームの値を合算したものを現在値みたいにするケースが殆どです。

// これだと使えない
var sum_stream = Kefir.scan(click_value_stream, function(acm, value){
  return acm + value;
});

しかしこれだと上手くいかないのです。

なぜならこちらが欲しいのは、下限と上限付きの値だからです。

言うまでもなく、下限は先頭ページ位置の0で、上限は「現在の」出力可能なページ数をマイナス1した値です。

ページ数が4(最大インデックス3)なのに、4とか5を出力されても困りますよね。

例えば2回だけ上限オーバーした数値は、PREVを2回押して戻さないと使えないってことになりますので。

だから単なる合算ではなく、現在のページ数を参照して合算するストリームが必要になります。

最初にミスったこと

これを実現するにあたって最初にミスったのはKefir.zipを使って、クリックのストリームとページ数のストリームを合成することでした。

しかしこれは上手く行きません。

なぜならクリックのストリームがclickイベントが発生する度に出力する無限ストリームであるのに対し、ページ数のストリームは最終ページを出力したらもう新しい値を出力しない有限サイズのストリームだからです。

有限のストリームと無限のストリームをzipすると、当たり前ですが短い方に合わせて有限のストリームが出来上がります。

つまりKefir.zipを使って合成したストリームを購読しても、有限回しか操作できません。

こういう2つのストリームを一つの無限ストリームに束ねるにはどうしたらいいのでしょう。

解決策「Kefir.combine」

そこでドキュメントをざっと眺めて見つけたのがKefir.combineです。

この関数は、対象ストリームの「最新の値」を使ってストリームを合成できます。

つまり片方の長さが3しかなくても、もう片方が無限なら、有限な方の最後の値が無限に参照できます。

結果、こんな実装になりました。

var page_index_stream = Kefir.combine([
  click_value_stream, 
  page_count_stream
], function(value, count){
  return {value:value, count:count};
}).scan(function(acm, cur){
  return {
    // 0 ~ [cur.count - 1]
    value:Math.max(0, Math.min(cur.count - 1, acm.value + cur.value)), 
    count:cur.count
  };
}, {value:0, count:0}).map(function(obj){
  return obj.value;
});

それぞれのストリームを購読

さて、なんとかpage_count_streampage_index_streamを定義することができました。

後は、page_count_streampage_index_streamをそれぞれ購読し、必要なUIを更新する処理を定義し、最後にページの計算を開始します。

するとページ出力のタイミングでpage_count_streamに最新のページ数がemitされるので、それに合わせて、紐付いたUIが連動して変化するようになっています。

var paged_element = Nehan.createPagedElement();

// ページ数を購読
page_count_stream.onValue(function(count){
  $("#page-count").html(count);
});

// UI操作とページ数によって決まる現在のページ位置を購読
page_index_stream.onValue(function(index){
  $("#page-no").html(index + 1);
  paged_element.setPage(index); // 現在位置を更新
});

// ページ計算を開始
paged_element.setStyle("body", {
  "flow":"tb-rl", // or "lr-tb"
  "font-size":16,
  "width":500,
  "height":300
}).setContent($("#screen").html(), {
  onProgress:function(tree){
    // ページ出力のタイミングで現在のページ数を出力
    page_count_stream.emit(tree.pageNo + 1);
  }
});

// 画面上に表示する
$("#screen").after(paged_element.getElement());

感想

色々なUI要素が絡むような複雑なアプリなら、注目している値を透過的に取得できるので、安全にUIを更新できそうな気がしますが、今回のような単純なケースでは少し面倒かもしれません。