anti scroll

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

10秒でわかるpseudo-classとpseudo-elementの違い

pseudo-class

pseudo-classは、左側のセレクターに続けて「〜が」と読みます。

/* liが最初の子だった場合 */
li:first-child{
  margin-top:0;
}

「〜が」だから、左側がマッチする要素です(上の例ではli)。

pseudo-element

pseudo-elementは、左側のセレクターに続けて「〜の」と読みます。

/* liの先頭文字 */
li::first-letter{
  font-size:3em;
}

「〜の」だから、右側がマッチする要素です(上の例では::first-letter)。

まとめ

どちらもpseudoと付いていますが、全く性質の異なるものです。

しかし古い仕様ではコロンがどちらも1つで記述されていて(例えば:first-letterとか)、紛らわしいという問題がありました。

なので最新の仕様では

  • pseudo-classにつくコロンの数は1つ(:first-childとか:last-childとか)
  • pseudo-elementにつくコロンの数は2つ(::first-letterとか::beforeとか)

というのが、正式な文法となっています。

ただし古いCSSと互換性を保つために、大体のブラウザではコロン1つでも通るようになっています。

余談

ちなみにnehan.jsの場合、コロンの数は厳密に区別します。

つまり::first-letterとすべきところを:first-letterと書いてしまうと、単に「未定義のpseudo-class」と見なされてしまいます。

Nehan.setStyle(".foo:first-letter", {fontSize:"3em"}); // NG(未定義のpseudo-class)
Nehan.setStyle(".foo::first-letter", {fontSize:"3em"}); // OK!

直感的に素直な感じでカスタムなリストマーカーを作りたい

例えば標準で用意されていないリストマーカーをUnicodeなんかで作りたいとき、以下のようにli::beforecontentを設定してマイナスのマージンをセットする、みたいなやり方がよく知られています。

ol li:nth-child(1)::before{
  content:'\2460'; /* CIRCLED DIGIT ONE */
}
ol li:nth-child(2)::before{
  content:'\2461';
}
/*
 (3〜9は省略)
*/
ol li:nth-child(10)::before{
  content:'\2469';
}
ol li::before{
  position:absolute;
  margin-left:-2.5rem;
}

しかし、このやり方ではnth-childの数字別にcontentを宣言することになるので、面倒だと思うのです。

もしかしたらcounterみたいなcss内の関数を使って色々できるのかもしれませんが…

nehan.jsを使った場合は、list-style-type関数値を設定することで、動的にunicodeを指定するスタイルがシンプルに記述できます。

Nehan.setStyle(".lst-circled-digit > li", {
  listStyleType:function(ctx){
    var index = ctx.getChildIndex();
    return String.fromCharCode(0x2460 + index);
  }
});

nehan.jsでは、list-style-typeが標準で定義された識別子ではない場合、css値がそのままマーカーとして使われるようになっています。

あるいは、::markerというli直下に差し込まれるpseudo-elementのcontentを動的に設定するやり方もあります。こちらのほうがわかりやすいかもしれませんね。

ただし::markerは公式のcssではまだdraftの扱いです。

Nehan.setStyle(".lst-circled-digit > li::marker", {
  content:function(ctx){
    var index = ctx.getParentStyle().getChildIndex();
    return String.fromCharCode(0x2460 + index);
  }
});

こうした上で、次のようなHTMLを組版させると

<ol class="lst-circled-digit">
  <li>あいうえお</li>
  <li>かきくけこ</li>
  <li>さしすせそ</li>
</ol>

縦書きと横書きで、それぞれ次のように表示されます。

f:id:convertical:20160313220725p:plainf:id:convertical:20160313220734p:plain

Unicode Hyphen覚え書き

例えばHTMLで「F-15」のような「-」(ハイフン)を含むboxを90度回転して表示させようとすると、ハイフンの後で改行されてしまいます。

f:id:convertical:20151231091605p:plain

なぜこんなことが起きてしまうのでしょうか。

理由

フォントサイズが16と仮定して「F-15」という横書きのwordを囲むサイズが、32x16(横x縦)だとします。

これを縦書き用に90度回転させると、このwordを囲むサイズは、32x16から16x32になります。

このときFの後のハイフンが、回転前の横幅(32)ではなく、回転後の横幅(16)を前提にして解釈されるのが、改行が発生する原因です。

「F-」まで描画したところでインラインの描画位置が、回転後における横幅の限界(すなわち16)に届くのですが、この位置で発生したハイフンが、line-break-pointとブラウザに判断されるため、改行が発生してしまうわけです。

Unicodeの中でline-break-pointと解釈されるハイフンは他にも色々(*注1)ありますが、よく使われるのはHYPHEN MINUS(U+002D)や、HYPHEN(U+2010)です。

*注1 U+002D, U+2010, U+2012, U+2013とかもそう。調べたらもっとあるかも。

というわけで、nehan.js内では英数字を縦書き用に回転させるときだけ、U+002DU+2010(U+2012,U+2013なども)を、あらかじめnon-breaking-hyphen(U+2011)に変換して、この問題を回避しています。

white-space:preでも解決できるかもしれないです。

もちろんこれはベストな解決方法ではありません。

こうしたHyphenの動きをコントロールするため、CSSにはhyphensというプロパティが定義されているのですが、なぜかChromeではまだ未実装(その他のブラウザは殆ど実装されている)なので、仕方なくこういう回避策をとっています。

CSSとDOMの境界、あるいはリスト組版がテーブル組版の親戚である件について

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

このデモですけど、ブラウザの大きさを変えると、リフローすることに気付いた人はいるでしょうか。

こういうデモで、テーブル組版や回り込み処理を見せると「おお」となる人が多いんですけど、リスト組版を見せても「で?」という人が多いと思います。

しかし、一見すると単純に見えるリスト組版ですが、実はテーブルの組版とよく似た複雑な組版なのです。

並列組版

nehan.jsでは、list-style-position:outsideかつdisplay:list-itemな要素と、display:table-rowな要素は、どちらもParallelGenerator(並列組版)のサブクラスで実装されています。

並列組版とは、複数の組版要素を同時並行的に行う組版です。

特徴としては、複数の子要素が矛盾なく表示されるための「前計算」を必要とするケースが多い、というのがあります。

例えばテーブルなら、各セルのサイズをどう割り当てるのか、事前に何らかの計算が必要なのは、なんとなく想像がつくと思います。

でもリストはどうでしょうか?

「リストなんて、単にテキストの先頭にリストマークの添字を足すだけじゃないの?」と思う人もいるかもしれません。

この指摘は、list-style-position:insideなら、まあそのとおりなのです。

しかしlist-style-position:outsideのときは、様相が異なります。

まずはUL>LIが書かれたHTMLを、適当なブラウザで表示させみてください。

<ul style="list-style-position:outside">
  <li>日本国民は、正当に選挙された(以下略)</li>
</ul>

CSSとDOMの境界

さて、表示されたリストですが、各リスト・アイテムの「リストマーク」は、マウス等ではテキスト選択できません。

f:id:convertical:20151203101342p:plain

つまり、このリストマークは、ユーザーがDOM操作では触れない組版要素になっています。

これが「CSSとDOMの境界」です。

これだけじゃありません。

list-style-positon:outsideのときは、リストの要素が一行を溢れても、リストマークの横幅だけ常に内容がインデントされています。

f:id:convertical:20151203101357p:plain

つまり空白の一列が割り込んでいるわけです。

この空白も、liマークアップに対するmarginpaddingで実現されているわけではないので、CSSとDOMの境界と言えます。

正確にリストを組版するためには、これらの境界要素に関するサイズ配分を事前に計算する必要があるわけです。

で、こういう性質を知っていると、例えばブラウザにとってどんなリストが意地悪なのかが、なんとなく予想できます。

意地悪な組版

例えばOL>LI*1000マークアップで、それぞれのリストを1em, 2em, 3emの文字サイズで順に表示させてみましょう。

<style type="text/css">
ol{ list-style-position:outside; }
.hoge{ font-size:1em }
.hige{ font-size:2em }
.hage{ font-size:3em }
</style>

<ol>
  <li class="hoge">hoge</li>
  <li class="hige">hige</li>
  <li class="hage">hage</li>
  <!-- 以下1000行まで続く -->
</ol>

どうしてこれが意地悪なのかというと、このリストを正しく表示するためには、1000行の添字が使うであろう数字文字列の最大幅を事前に計算する必要があるからです。

じゃないと、適切なインデントを実現できませんよね。

しかしchrome/firefox/safariにこれを表示させると、いずれも桁数が大きくなったところで、以下のようにリストの添字部分が「画面の左側」に見切れてしまいます。

f:id:convertical:20151203101412p:plain

body{margin-left:3em}みたいな設定をしていたってダメです。左の限界を突破して表示が切れます。

もちろん普通に考えると、li要素にそれぞれ違う文字サイズを付与することなんて考えにくいことなので、実用上は問題ないのです。

ただ正確に扱おうとするなら、リスト要素もテーブル組版と同じく、先に必要領域を計算する必要のある重い組版に該当するわけです。

この1000行のリストが、現状のブラウザで瞬時に表示されるのは、単に各ブラウザが厳密な計算をサボっているだけで、組版処理そのものが単純だからではありません。

というわけで、リストの組版も(テーブル組版ほどではないですが)、なかなかに複雑な処理なのです、というお話しでした。

いつの間にかFileAPIのreadAsBinaryStringがオワコンになっていた。今後はreadAsArrayBufferで。

タイトルで全てを言い尽くしてしまいましたが、経緯をば。

不具合報告で「IEで画像のアップロードができない」ってのがあったので調査してみたら、readAsBinaryStringでコケてました。

調べてみると、どうもIE系列ではreadAsBinaryStringそのものが存在しないらしく…

なんでなのかというと、そもそもこのAPI、将来的になくなる方向らしいのです(実際にw3cのドキュメントからも消えてる)。

というわけで、readAsBinaryStringreadAsArrayBufferに置き換えたら、IE11でも(もちろんChrome/Firefoxでも)問題なく動いたのですが…

非推奨になった理由

で、なんで非推奨となったのかというと、単に効率が悪いからだそうで。

https://lists.w3.org/Archives/Public/public-webapps/2011OctDec/1497.html

要約すると「このメソッドは型付きの配列(ArrayBuffer)が標準化される前に作られた実装で、バイナリデータを文字列に変換するから効率が悪い」とのこと。

今は型付きの配列は既に実装されていて、それを読み取る関数(readAsArrayBuffer)も実装済み。したがって、非効率的な古いAPIに存在意義はない(から削除するべきだ)、ということみたいです。

というわけで、これからはreadAsArrayBufferなのです。

Blobを作らなくても、fileオブジェクトならそのまま渡せます。

RxJSとvirtual-domで、拡張可能なreactiveアプリケーションを作るリアクティブ・アダプター - inga

inga(因果)は、軽量で拡張可能なリアクティブ・アダプターです。RxJSとvirtual-domを使っています。

github.com

よくあるTodo-MVCは100行ぐらいです。

inga/todo-mvc.js

特徴

  • 軽量なこと。全ソース合わせても100行ちょっとしかないです。
  • プラグインで拡張可能です。
  • 副作用の処理をthunkで扱うストリームがデフォルトで付いているので、大抵のイベント処理はこれで済みます。

簡単なサンプル

例えばクリックする度にカウンタが増える、みたいなのは次のようになります。

var Inga = require("inga");

Inga.define({
  domRoot:"#app",
  dataSource:new Inga.ActionStateStream({
    initialState:{clickCount:0}
  }),
  virtualView:function(ctx){
    return h("button", {
      "ev-click":function(ev){
        ctx.action.emitUpdater(function(state){
          state.clickCount++;
        });
      }
    }, ctx.state.clickCount);
  }
});

プラグインを使ったサンプル

例えばinga/plugins/scroll-posを組み込むと、こんな感じでレンダリング・コンテキストの状態オブジェクトにscrollPosが合成されます。

var Inga = require("inga");

Inga.define({
  domRoot:"#app",
  dataSource:new Inga.ActionStateStream({
    initialState:{scrollPos:{x:0, y:0}},
    // plugin 'scroll-pos' combines 'scrollPos' to status object.
    plugins:[
      {module:require("inga/plugins/scroll-pos"), options:{combine:true}}
    ]
  }),
  virtualView:function(ctx){
    return h("div", [
      h("p", "x = " + ctx.state.scrollPos.x),
      h("p", "y = " + ctx.state.scrollPos.y)
    ]);
  }
});

自前のストリームを合成するサンプル

デフォルトのストリームに、自分で作ったストリームを組み込む場合は次のようにcombineアクションを定義します。

var Rx = require("rx");
var Inga = require("inga");

Inga.define({
  domRoot:"#app",
  dataSource:new Inga.ActionStateStream({
    initialState:{width:window.innerWidth},
    actions:{
      // extend default stream by your own source.
      combine:function(initial_state, upstream$){
        var width$ = new Rx.Observable.fromEvent(window, "resize")
          .debounce(250)
          .map(function(event){
          return window.innerWidth
        })
        .startWith(window.innerWidth);

        return upstream$.combineLatest(width$, function(state, width){
          state.width = width;
          return state;
        });
      }
    }
  }),
  virtualView:function(ctx){
    return h("p", "current window width = " + ctx.state.width);
  }
});

その他

ライセンスはMITです。

nehan.jsのセレクターマッチング処理を高速化

nehan.jsのセレクタのマッチング処理を高速化しました。

先日サポートした行末揃えは、有効にすると20%ほど遅くなりましたが、今回の修正によって15%ほど速くできたので、少し戻すことができました。

前々から、機会があったらやっておこうと思っていた処理でもあったし、ちゃんと効果があって良かったです。

最適化の内容

やっていることは単純で、セレクタのマッチング結果を(キャッシュできるものに限り)キャッシュしただけです。

ただしマッチ結果には、キャッシュしやすいものとしにくいものがあるので、その辺のことについてちょっと説明します。

属性セレクタのマッチング結果はキャッシュが困難

属性セレクタは、マークアップや親が同じでも、マークアップが現れる相対的な文脈によってマッチしたりしなかったりするので、常に同じ値が返ってくるとは限りません。

これに対応するためには、属性クラスの情報をキャッシュキーに盛り込む必要があります。

しかし例えば次のようなマークアップを考えてください。

<div>
  <p class="foo">text0</p>
  <p class="foo">text1</p>
  <p class="foo">text2</p>
</div>

<div>
  <p class="foo">text3</p>
  <p class="foo">text4</p>
  <p class="foo">text5</p>
  <p class="foo">text6</p>
</div>

ここでtext2text6を内容に含むpタグをそれぞれp2p6と名付けることにします。

さて、p2p6のキャッシュキーはどうなるでしょうか。

両者の特徴をフルに含んだキャッシュキーを設計するとなれば(擬似的な記述ですが)こんなかんじになりそうです。

// p2のキャッシュキー
div>p.foo[nth-child=2, first-of-type=false, last-child=true, last-of-type=true, ...]

// p6のキャッシュキー
div>p.foo[nth-child=3, first-of-type=false, last-child=true, last-of-type=true, ...]

殆ど同じキャッシュキーですが、nth-childの値だけが異なります。

しかし両者は木構造の位置として、性質的には殆ど同じものです(divの直下にあるpタグの最後の要素)。

なので、p2p6のどちらも、次のセレクタにマッチします。

div>p.foo:last-child{ font-size:2em }

しかし、nth-childの値が違うだけで、違うキャッシュキーとなるわけです。

これを同じキャッシュキーにするわけにはいきません。なぜなら属性クラスはnth-childを使っている「かも」しれないからです。 属性クラスをキャッシュして(他の場所で)再利用するなら、確実に属性クラスが表現しうる全ての特徴が一致している、という保証が必要なのです。

というわけで、仮にp2のマッチ結果をキャッシュしたところで、同じ結果がp6のマッチング時に再利用されることはありません。

キャッシュが効果的なのは、どれだけ再利用されるかにかかっているのですが、こうやってキャッシュが分散すると、ヒット率が落ちる上に容量も食うわけで、かえって性能を損ねるおそれが出てきます。

というわけで、キャッシュキーが分散しがちな属性セレクタについては、都度マッチする方針で統一しました。

ただし、属性クラスを含むセレクタは別の領域に保存しておくなどして、前もって検索範囲を狭めておく、というようなことはしています。

キャッシュできる属性クラスのセレクタを書く方法

しかし、上記のようなp2p6で、同じマッチ結果がキャッシュされるようなセレクタが書きたい! と思いませんか?

nehan.jsならこれができます。

nehan.jsでは、cssプロパティに関数値を設定できるので、先ほどと同じ場所(div>p.foo:last-child)を、属性クラスを使わずに指定することがでるのです。

例えば以下のように、属性クラスの判定を遅延評価の関数に追い出すことで、セレクタの詳細度が減った「キャッシュヒット率の高い」セレクタを宣言することができます。

"div>p.foo":{
  onload:function(ctx){
    if(ctx.isLastChild()){ // is last-child?
      return {fontSize:"2em"};
    }
    return null;
  }
}

大まかなものでマッチさせておくことでセレクタのマッチング結果をキャッシュさせ、last-childのような詳細で決まるスタイルについては、遅延評価で取得させるわけです。

一見すると、スタイル値の取得に際し関数呼び出しのオーバーヘッドが発生するように見えますが、これは属性クラスの条件判定をセレクタ検索時に計算するか、検索後に計算するかの差でしか無いので、性能差には結びつきません。

属性クラスバージョンと遅延評価バージョンの性能差

とはいえ、セレクタ数が少ないケースでは、どちらの書き方をしても、たいして性能の差はありませんでした。

字数が40万字、セレクタ数が5000個ぐらいのラインを超えると、さすがにキャッシュの効く遅延評価バージョンの方が22%ぐらい速かったですが…

マージ後のスタイル値はキャッシュ出来ないのか

マッチしたセレクタは、specificityの順にソートし、最終的なスタイル値へとマージされます。

この最終的なスタイル値をキャッシュできると更に良いのは言うまでもないことですが、nehan.jsではcssの値に関数を指定できる仕様がある関係上、これが難しくなっています。

関数値で指定された値は実行時に値が決まるサンクなので、ある特定のタイミングで呼ばれて返ってきた結果を、永続的な最終値としてキャッシュすることはできないからです。

そもそも仮にキャッシュしたとしても、スタイル構築で時間がかかるのは圧倒的にセレクタ検索の部分なので、大した効果は期待できないと思われます。

Chrome Extensionのmanifest.json内version値を更新するスクリプト

手作業でやるのが面倒になってきたので書きました。

事前にnpm install minimistが必要です。

// update-manifest-version.js

var fs = require("fs");
var args = require("minimist")(process.argv.slice(2));

var update_version = function(version, target){
  var parts = version.split(".");
  var map = {major:0, minor:1, build:2, revision:3};
  var index = map[target] || 2;
  parts[index] = parseInt(parts[index]) + 1;
  return parts.join(".");
};

var file = args.f || "./manifest.json";
var target = args.t || "build";
var encoding = args.e || "utf8";
var json = JSON.parse(fs.readFileSync(file, encoding));

json.version = update_version(json.version, target);

fs.writeFileSync(file, JSON.stringify(json, null, 2), "utf8");

使い方

例えば今のバージョンが1.2.5(major=1, minor=2, build=5)で、buildバージョンだけ1つ上げたい場合

node update-manifest-version.js -f /path/to/manifest.json -t build -e utf8

とやります。