anti scroll

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

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

Nehan Reader 0.9.20をリリース

どんなページも、縦書きや横書きのページ送りで読めてしまうGoogle Chrome拡張「NehanReader」のversion0.9.20をリリースしました。

chrome.google.com

実際に青空文庫の作品を表示すると、こんな感じに。

f:id:convertical:20150307115543p:plain

Yahoo News!とかだと、こんな感じに。

f:id:convertical:20150307131435p:plain

f:id:convertical:20150307131448p:plain

修正点は以下のとおり

  • たまに同じ文字が繰り返し表示されてしまうケースを修正
  • これまでサイズの指定されてない画像を固定で小さく表示していたものを、事前にサイズを取得して表示するように変更

マイナーバージョンが20と、なんだかおかしなことになっていますが、まだ1.0.0を宣言する自信はないので、もうしばらく上がっていくと思います。

(追記)なお公開して一時間たらずで0.9.21がリリースされた模様。

「貼るだけ」のアクセス解析?

今回アクセス解析を実装するにあたって疑問に思ったのですが、よくある「貼るだけでOK」なアクセス解析って、サイト所有者の確認ステップがないっぽいのがありますが、これって大丈夫なんでしょうか。

例えば誰かが勝手に他人のサイトを先に登録したら、後から正当なサイト所有者が登録しようとしても「既に登録されています」とかで弾かれてしまうのでは。

仮に同じサイトを登録できる仕様にしたとしても、今度はサイトの所有者でもない人が同じサイトのURLを登録して、アクセスを覗ける仕様になっちゃいますよね。

だって、どっちが正当な所有者かを検証していないので、両方に見せるしかないじゃないですか。

かといって「先に登録した人に見せる」という仕様にしたとしても「先に登録した人」=「サイトの所有者」であるとは限らないわけで。

つまりですね、確かに「解析タグを貼ること」はサイトの所有者にしかできないけれども、それはサイトの所有者を確認するステップを省いていい理由にはならんのでは…ということです。

もちろんウェブマスターツールとかgoogle analyticsは、サイトの所有者であることが確認されないと使えませんよね。先日公開したNovelyticsもそうです。

それとも貼るだけで所有者を確認する方法があるのでしょうか。僕が知らないだけで。

もしそうだったら誰か教えて下さると嬉しいです。

「Novelytics」の紹介 〜なぜ小説専用のアクセス解析が必要なのか〜

小説専用のアクセス解析

Novelyticsという小説専用のアクセス解析サービスを作りました。

なぜこんなものを作ったのかについて書きます。

アクセスをベースにしたログの限界

アクセス数というのは、言い方を変えれば単なるクリック数です。

当たり前ですが、これは「読まれたこと」を意味しません。

ページ送りの所要時間や、ページに含まれる字数などを考慮に入れず、ただクリックされただけのログで読者の反応をイメージするのは、非常に難しいことです。

しかしアクセスログが最終ページを開いたログを残していたら、作者は「読了された」と勘違いしてしまうのではないでしょうか。

しかし読者がいかに飽きっぽく冷たいのかは、縦書き文庫の読者分析グラフを見るとよくわかります。

だいたいどんな人気作品でも、先頭を開いた人が10とすると、最後まで読むのは1ぐらい。そんなものなのです。

というわけで小説専用のアクセス解析として作ったのがNovelyticsです。

このサービスでは、アクセス数ではなく「ページ送り」でログを取ります。

飛ばし読みは記録されず、信頼性のあるページ送りだけが「ページ番号」「ページ位置」「ページの字数」「所要時間」と共に記録されます。

f:id:convertical:20150305130900p:plain

グラフも出るので、どの位置で離脱するのか、どの位置までは読まれたのか、などがわかります。

f:id:convertical:20150305130919p:plain

使い方

1. ユーザー登録

ユーザー登録は、グーグルアカウントで行います。

グーグルアカウントがあれば、数クリックで登録は完了します。

2. サイトの登録

最初にすることは、アクセス解析を貼り付けるサイトを登録することです。

サイトのタイトル(後で編集可能)と、トップページのURL(後から修正できない)を登録します。

URLを修正する場合は、サイトの登録をいったん削除して作り直すことになるので注意して下さい。

3. サイトの認証

登録したサイトは一度だけ認証が必要です。

サイト一覧から「認証する」を押して下さい。

認証は、サイトのトップページにて表示される<head>タグの中に、指定されたメタタグを貼り付けることで行います。

はてなブログなら

  1. ブログの管理画面から「設定」を選択。
  2. 設定の中から「詳細設定」タブを選択。
  3. 詳細設定の「headに要素を追加」に指定されたコードを貼り付ける。

という手順です。

面倒かもしれませんが、第三者によるなりすましを防ぐためには、どうしても必要な認証なのです…

4. アクセス解析用コードの貼り付け

ブログのテンプレートなどを編集して、「アクセス解析用コード」をブログの</body>の直前に貼り付けます。

編集領域として</body>の直前が編集できないなら「フッター」とか「サイドバー」とかでもOKです。ただし</body>の直前が(ページの表示を妨げないという意味で)ベストです。

アクセス解析用のコードは、メニューから「埋め込みコード」を選択し「アクセス解析用コード」にあるコードを使って下さい。

はてなブログなら「デザインの編集」から「フッター」を編集して、コードを貼り付けたらいいと思います。

(追記)ただし、はてなブログのモバイル用のページはPROアカウントじゃないとフッターが編集できないっぽいです。

5. 「本にして読む」ボタンの設置

これで準備は整いました。

あとはページ送りを記録したいブログ記事の先頭に「本にして読むボタン」のコードを貼り付けて終了です。

「本にして読むボタン」のコードは、メニューから「埋め込みコード」へと進んで取得して下さい。

ボタンの動作をカスタマイズしたい人は、ヘルプから「ボタンのカスタマイズ」を参照して下さい。

意外と色々な設定ができます。

6. 動作テスト

まずは自分でページ送りしてみて(ただし飛ばし読みは無視されるので、時間をかけてページを送る)、実際に記録されることを確認したら、設定ページから自分自身のIPを設定すると良いと思います。

最後に

こういうログサービスは、縦書き文庫においては2006年ぐらいから標準で提供している機能なのですが、一般化したら面白いかなあと思って作りました。

ドラッグ・アンド・ドロップで、テキストを縦書きページ送りで表示するサイト「Nehan Text Reader」

HTMLファイルやテキストファイルをドラッグ・アンド・ドロップすると、縦書きページ送りで表示してくれるサイト「Nehan Text Reader」を公開しました。

tb.antiscroll.com

画面上(どこでもいい)にテキストファイルをドラッグ・アンド・ドロップすると、ビューアーが表示されます。

文字化けする場合は「表示設定」の「文字コード」を変更して、もう一度ドロップしてください。

テキストファイルで執筆してる人は、原稿の確認とかするのに使えるかもしれません。

ちなみに表示設定から横書きにもできます。

実際に放り込むとこんな感じに。

f:id:convertical:20150211173437p:plain

テキストファイルであればなんでもよくて、htmlファイルを放り込めばアウトラインも出ます。

単純なマッチで複数回replaceするのと、文字クラスを使って一回replaceするのでは、どちらが速いか

qiitaでこんな記事がありました。

innerText(textContent)/innerHTMLを使わずJavaScriptでHTMLエスケープ - Qiita

で、思い出したのですが「文字クラスでreplaceを一度で済ますより、単純なマッチを直列で繰り返したほうが速い」って話しをどこかで聞いた覚えがあるので、どのぐらい差があるのか、ちょっと試してみました。

念のため、元記事の関数でテーブル参照をしないバージョン(escapeCharClassEx)も用意。

3パターンのescape関数

// 元記事の関数
var escapeCharClass = function(content) {
  var TABLE_FOR_ESCAPE_HTML = {
    "&": "&amp;",
    "\"": "&quot;",
    "<": "&lt;",
    ">": "&gt;"
  };
  return content.replace(/[&"<>]/g, function(match) {
    return TABLE_FOR_ESCAPE_HTML[match];
  });
};

// 念のためテーブル参照しないバージョン
var escapeCharClassEx = function(content) {
  return content.replace(/[&"<>]/g, function(match) {
    switch(match){
    case "&": return "&amp;";
    case "\"": return "&quot;";
    case "<": return "&lt;";
    case ">": return "&gt;";
    }
  });
};

// 4回replaceバージョン
var escapeSerial = function(content){
  return content
    .replace(/&/g, "&amp;")
    .replace(/"/g, "&quote;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
};

検証プログラム

以下の様な感じで、入力文字が短い場合と長い場合の双方で速度を比較してみました。

var replicateString = function(text, times){
  var ret = [];
  for(var i = 0; i < times; i++){
    ret.push(text);
  }
  return ret.join("\n");
};

var testEscapeSpeed = function(){
  var text = "hoge&hage>hige<hoge";
  var text_short = replicateString(text, 10);
  var text_long = replicateString(text_short, 100);
  var replace_many = function(text, replace_fn, times){
    times = times || 1000;
    for(var i = 0; i < times; i++){
      replace_fn(text);
    }
  };

  console.time("char class short");
  replace_many(text_short, escapeCharClass);
  console.timeEnd("char class short");

  console.time("char class short ex");
  replace_many(text_short, escapeCharClassEx);
  console.timeEnd("char class short ex");

  console.time("serial short");
  replace_many(text_short, escapeSerial);
  console.timeEnd("serial short");

  console.time("char class long");
  replace_many(text_long, escapeCharClass);
  console.timeEnd("char class long");

  console.time("char class long ex");
  replace_many(text_long, escapeCharClassEx);
  console.timeEnd("char class long ex");

  console.time("serial long");
  replace_many(text_long, escapeSerial);
  console.timeEnd("serial long");
};

検証結果

Chrome40で走らせた結果、以下のような結果になりました。

test name time
char class short 7.573ms
char class short ex 4.213ms
serial short 1.972ms
char class long 296.516ms
char class long ex 264.621ms
serial long 128.274ms

単純なマッチで4回replaceしたバージョン(escapeSerial)が、短い文字列では4倍程度、長い文字列だと2倍ちょっとぐらい速いという結果でした。

いや、ちょっとおかしい

文字クラスを使った場合の関数は、単にマッチ後のコールバック関数呼び出しのオーバヘッドが入るだけではあるまいか、ということでescapeSerialにおける各replace処理を関数で置換するように変えたら、今度は4回呼び出しのほうがコールバックの回数が増えたぶん、少しだけ遅かったです。

test name time
char class short 5.004ms
serial short 5.218ms
char class long 2559.319ms
serial long 3020.997ms

ということで、

  1. 文字クラスで複数文字を一回でマッチ&置換させても、それぞれの文字で一回ずつ置換しても速度は殆ど変わらない
  2. しかしそれぞれの置換でコールバック関数を呼ぶとパフォーマンスは二倍ぐらい遅くなる
  3. 各関数の呼び出しの中でテーブル参照すると更に遅くなる

みたいです。

たぶん1については、4×1と1×4程度の違いなんだと思います。