anti scroll

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

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属性が入っていない時は無効だから何もしない」みたいな処理を加えても良いと思います。

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

結論

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

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

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

OCaml用の麻雀ライブラリ

フォルダ整理していたら、二年ほど前に書いたOCaml用の麻雀ライブラリが発掘されました。

しまっておくのも無駄だし、バックアップも兼ねてgithubに上げておくことに…

https://github.com/tategakibunko/ocaml-mjlib

一応、ゲームやプレイヤーなども含めて抽象化されている(っぽいけど忘れた)。

ちゃんとユニットテストも付いてるから、それなりに動くんだと思います。

縦書き文庫では、ろくにテストもせずに上げ、苦情ドリブンで対応する僕ですが(個人的にソーシャルデバッグと呼んでいる)、 こういうロジック系のライブラリでは流石にユニットテストぐらいは書きます。

ちょい見る限り、どこか抜けはあるかもしれませんが、日本ルールの役は一通り得点計算できてる(はず)。

ちなみに、よくブログ記事で麻雀の処理を書いてみた、みたいなので、アマタ、シュンツ、コーツをパースして「はい、おしまい」みたいな人をよく見かけますけど、それって麻雀全体の得点計算ロジックの1割にも満たない部分ですからね。

というのも、麻雀って、手元にある14牌の状態だけで判断できる役とか得点なんて殆どないのです…

得点を正しく計算しようと思ったら、とにかく色々なコンテキストを検証する必要があるので。

まずそもそも手元の14牌だって、最後の一枚がツモなのかロンなのかってので区別する必要がありますよね。

ツモかつ喰いがなければ「ツモ」なる役が付くわけですし。

さらにプレイヤー同士の席順で判断する必要のある役とか点とかルールもあるから(リーチ一発とか、ダブロンとか、チーはカミチャだけとか)プレイヤーを抽象化する必要もあるし。

面子だって、鳴かれたものなのかどうかで府の計算も変わるし、役の扱いも変わってくるし(じゃなきゃ、ただのトイトイがスーアンコになってしまう)。

また上がる前の「待ち」がどうだったかによって付く役と付かない役とか符もある。

リャンメン待ちじゃなけりゃピンフにならないし。

リャンメンじゃないならないで、そういう場合は待ちには符がついて得点が変わるし。

また場風で決まる役もあるから(風牌とかダブトン、ダブナンとか)、ゲームの進行状況も抽象化しないと正しい得点計算が出来ない。

他にも裏ドラとかリンシャンとかハイテイとかチョンボとか…まあ色々です。

とにかくゲームの状況(コンテキスト)で決まるものが多すぎるので、必然的に色々なものを抱え込んで抽象する必要があるわけです。

例えばカレンダーを扱うライブラリだってそうですよね。

単に暦とか言ったって、見る観点が色々とあるので。

時間として扱うのか、日付として扱うのか、それともある日付を基準として数えた現在までの秒数として扱うのか。表示フォーマットだって、国ごとに色々あります。

麻雀ライブラリもそういう感じです。

ちなみに麻雀ゲームは過去に色々な言語で作ったことがあるのですが(個人的な趣味です)、 一番楽に書けたのはOCamlだったと思います。

過去の組み合わせは、サーバー/クライアントの組み合わせで

とかです。

まあ経験値が上がったぶん、最新のものが楽になってるのは当然かもしれないですけど。

これ以外には、Haskellの自習用で作った簡易麻雀ライブラリ(hml)もありますが、HaskellOCamlと同じぐらい書きやすかったかもです(特にShowって型クラスが便利だった)。

ちなみにこのライブラリ、最近になって妙にforkされているのですけど、Haskellの手習いとして作ったものなので純粋にヘタクソだし、そもそも大きな山の一割ぐらいしか上っていないので「いいんかな?」という感じです。

外部の埋め込み作品に独自のスタイルを設定する方法

外部の埋め込み作品に、独自のスタイルを設定する方法を用意しました。

具体的には、作者が自前で用意したスタイルファイルを外部から読み込ませる形になります。

通常の埋め込みコード

まず通常の埋め込みコードですが(先頭部分だけ抜粋すると)こんな感じになっていると思います。

var TbThumb = {
  nid:2980,
  width:640,
  //width:"80%",
  height:480,
  fontSize:20
};

大きさの設定ぐらいは出来るけれども、それ以外の細かい設定はできませんでした。

そこで、こんな風にしてスタイル用のスクリプトを指定できるようにしてみました。

スタイルスクリプトを指定した埋め込みコード

var TbThumb = {
  theme:"http://yoursite/path/to/theme.js",
  nid:2980,
  width:640,
  //width:"80%",
  height:480,
  fontSize:20
};

新しく追加されているのは、

theme:"http://yoursite/path/to/theme.js",

の部分です。

"http://yoursite/path/to/theme.js" の部分はお使いのサーバーのURLに置き換えてください。

theme.jsの中身は例えばこんな風に書きます。

Nehan.setStyles({
  "body":{
    "font-size":"16px",
    "padding":{
        "start":"10px",
        "end":"10px"
    },
    "border-color":"wheat",
    "border-style":"solid",
    "border-width":"5px",
    "border-radius":"5px",
    "background-color":"wheat"
  }
});

このスタイルで埋め込んでみた結果

背景を変えて角を丸くしただけですが、再生すると普段とは違う表示になります。

注意事項

スタイルを設定する際にとりあえず覚えておいてほしい点を二つ上げておくと、

  • CSSプロパティはfont-sizeのようにハイフンを使った書き方になる点
  • margin/border/paddingなどを指定する方向として論理方向(start/end/before/after)を使っている点

です。

CSSのプロパティ名については、例えばfont-sizeのことをfontSizeのようには書けない、ということです。

論理方向については、日本語の縦書き(tb-rl)ならstart/endが上と下、before/afterが右と左、とでも覚えておいてください。

より具体的なスタイルスクリプトの書き方

興味のある人はhttp://tategakibunko.github.io/nehan.js/を参照してください。

nehan.js version5 を試験運用中

今週から縦書き文庫やauto-machicなどで、nehan.js version5系列(ベータ版)を試験的に運用しています。

nehan-readerもversion5をのせてver0.7.6を公開しました。

ということで、しばらくは何かしら表示上の問題があるかもしれないです。

ちなみにversion5以降は、IE8以下のテストはしていません。

おそらく動くとは思いますが、テストをしないのですから、事実上のサポート打ち切りと言ってもいいでしょう。合掌……!

はじめて評価をもらった件

GoogleChrome拡張のNehan Readerですが、初めて評価をもらいました。星五つ! 嬉しいですね。

中国語なので内容はよくわかりませんが、英語の閲覧はともかく日本語の閲覧には便利だ、みたいなことだと思います。