anti scroll

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

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

必然性は全くなかったのですが、Fluxを試してみたかったので作ってみました。

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

使ってみた感想

あんまり感触は良くないかも…

ただ今はなんとなく全体像を掴んだかもっていう段階なので、もっと複雑なUIを作るとなったら、こういうアーキテクチャが生きてくるケースもあるのかも? しれません。

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

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

http://www.sakaguti.org/honmon%20page/top%20page/top%20page.htm

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

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

ただし、そのままだと読みづらい気がしたので、自分は 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

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

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

[2014/02/14 追加]

現在サイトは閉鎖されてしまったようですが、以下の本などで同じような内容が読めるようです。

*1:2017/02/14 現在は閉鎖されています。

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 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に作る

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

ビューアーの要件

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

実装方針

  • 非同期で新しいページが出力されるたびに「現在の総ページ数」が出力されるストリーム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を更新できそうな気がしますが、今回のような単純なケースでは少し面倒かもしれません。

サイズの指定されていない画像タグにサイズを付けるjQueryプラグイン

既にあるのかもしれませんが、だからといってどうやって検索したらいいかわからないものは自分で作るしかない…ということで表題のものを作りました。

tategakibunko/jquery.image-size-assign · GitHub

概要

どういうものかというと、ようするに以下のようなことをするものです。

<!-- before -->
<img src="http://placehold.it/350x150">

<!-- after -->
<img src="http://placehold.it/350x150" width="350" height="150">

使い方

こんな感じです。

$(function(){
  $("img").imageSizeAssign({
    maxSize:{
      width:500,
      height:500
    },
    onSize:function(size){
      return size;
    },
    onComplete:function(){
      //console.log("all sizes are set");
    }
  });
});

オプションにmaxSizeを指定すると、あふれた時に元サイズのレートを保ったままリサイズします。

例えば上の例だとmaxSizeが500x500なので、1000x1000の画像だったら、500x500にリサイズされます。

参考にしたソース

javascript - Can I sync up multiple image onload calls? - Stack Overflow