anti scroll

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

jingoo 1.4.0 をリリースしました

jingoo 1.4.0をリリースしました。

github.com

新しくなった点

  • Jg_templateモジュールを部分的に改良したJg_template2というモジュールが追加されました。

サンプル

これまでJg_templateモジュールは、次のようにmodels連想リストを与えていました。

open Jingoo
open Jg_types

let result = Jg_template.from_string "{{ msg }}" ~models:[
 ("msg", Tstr "hello, world!");
]

Jg_template2モジュールからは、models名前を与えたら値を返す関数を渡します。

open Jingoo
open Jg_types

let alist = [("msg", Tstr "hello, world!")]
let result = Jg_template2.from_string "{{ msg }}" ~models:(fun key ->
  try List.assoc key alist with Not_found -> Tnull
)

これの何が良いのかというと、string -> tvalueという型の関数にさえなっていれば、データソースは何でも良くなることです。

例えばデータソースを(連想配列より高速な)Hashtblにすることもできます。

open Jingoo
open Jg_types

let htbl = Hashtbl.create 1 in
let () = Hashtbl.add htbl "msg" (Tstr "hello, world!") in
let result = Jg_template2.from_string "{{ msg }}" ~models:(fun key ->
  try Hashtbl.find htbl key with Not_found -> Tnull
)

データソースなしで、単なる関数にしてしまうと、さらに高速です。

open Jingoo
open Jg_types

let result = Jg_template2.from_string "{{ msg }}" ~models:(function
 | "msg" -> Tstr "hello, world!"
 | _ -> Tnull
)

関数にすることで、乱数値みたいなものを扱うこともできます。

open Jingoo
open Jg_types

let result = Jg_template2.from_string "{{ msg }}, {{ rand }}" ~models:(function
 | "msg" -> Tstr "hello, world!"
 | "rand" -> Tint (Random.int 100) (* 0 ~ 100 の値をランダムに返す *)
 | _ -> Tnull
)

将来的にはこっちをメインのAPIとして使用したいのですが、旧版のAPIで既に広く使用されてしまっているので、今回はモジュールを別名で分けることで対応しました。

参考

In Jingoo.Jg_template, provide a function for the getting variables · Issue #112 · tategakibunko/jingoo · GitHub

nehan7をリリース

nehan6からnehan7にバージョンアップしました。

インストール

npm install --save nehan

変更点

  • 組版速度が約20%向上しました。
  • コードサイズが約10%削減されました。
  • nehan.cssが不要になりました。
  • 行内に置換要素、画像、ルビ、圏点傍点、複数サイズの文字などを同時に含むような場合でも、ベースラインが正確に計算されるようになりました。
  • テーブルセルに対してvertical-align: middleが効くようになりました。
  • いくつかのパターンにおける文字詰めの表示崩れを修正しました。
  • 行末揃えの処理を指定しても組版速度が落ちなくなりました。
  • PageReaderは廃止されました。今後はPagedHtmlDocumentを使用してください(後述)。

縦書き文庫のビューアーの刷新

縦書き文庫のビューアーも、新バージョンのエンジンに差し替えました。

nehanのコードサイズが減ったぶん、ページの読み込みも(少しだけ)速くなったと思います。

500KByteほど削減されているので、モバイル環境なんかでは、そこそこ効果が大きいのではないでしょうか。

UIについても、ちょっとだけ刷新しました。

これまでは「本文」と「目次・登場人物」の2列で表示していたのですが、今後は本文を一列(ワンカラム)で表示するようにして、「目次・登場人物」は本文下のメニューに移動させました。

これにより、本文は表示領域が広くなり見やすくなった一方、目次や登場人物に対する操作性は悪くなってしまったので、これに関しては今後の課題としておきます。

(本文の横に目次やら人物やらのタブを表示したらいいのかなあ、と考えています)

nehan7のざっくりとした使い方(開発者向け)

だいたいこんな感じで使います。

import { PagedHtmlDocument, CssStyleSheet } from 'nehan';

const src = "<h1>hello, nehan!</h1>";
const doc = new PagedHtmlDocument(src, {
  styleSheets: [
    new CssStyleSheet({
      "body": {
        "writing-mode": "horizontal-tb", // or "vertical-rl"
        // インライン方向のサイズ(横書きならwidth、縦書きならheightに相当)
        "measure": "450px", 
        // ブロック方向のサイズ(横書きならheight、縦書きならwidthに相当)
        "extent": "500px",
        "font-size": "16px",
        "padding": "1em"
      }
    })
  ]
});
const $dst = document.querySelector("#dst"); // 結果を格納するDOM

// 描画を開始(非同期処理)
doc.render({
  onPage: (ctx){
    const page = ctx.caller.getPage(ctx.page.index); // ページを評価
    $dst.appendChild(page.dom); // 評価したページのDOMを注入
    $dst.appendChild(document.createElement("hr")); // 一応、分割線
  },
  onComplete: (ctx){
    console.log(`終わりました(${ctx.time}msec)`);
  }
});

注意事項1(論理プロパティについて)

nehanは論理組版エンジンなので、領域サイズの指定にwidthとかheightなどは使えません。

代わりにmeasureとかextentという論理プロパティで指定します。

measureは横書き(writing-mode:"horizontal-tb")ではwidthのことです。

縦書き(writing-mode:"vertical-rl")ならheightになります。

extentは横書き(writing-mode:"horizontal-tb")ではheightのことです。

縦書き(writing-mode:"vertical-rl")ならwidthになります。

同じく、top, right, bottom, leftといった名前は、nehanではそれぞれbefore, end, after, startと指定します。

例えば横書きのmargin-leftは、nehanではmargin-startです。

before/afterはブロック方向の前後で、start/endはインライン方向の前後、と覚えてください。

注意事項2(段組とページ送りについて)

上のサンプルでは、appendChildを使って、組版結果をどんどん追記する「段組み方式」で表示していますが、ページ送りで表示したいときは、自前で現在表示するページやページ番号などを管理する必要があります。

その場合、起動時には先頭ページだけを表示し、それ以降のページはページ送りをしたタイミングでdoc.getPageを使って動的に対象ページを取得し、画面上のDOMを差し替える、という実装になるでしょう。

ページ送りの場合の実装については、サンプルのbook.tsなどが参考になるかもしれません。

Angular(<=8)でnehanを使っている方への注意

結論だけ先に述べておきます。

  • Angular8以下では、nehan(<=6.0.38)までしか使えません。
  • ただしTypescript3.6.3以降を使える環境では、なんの問題もなくnehan(>=6.0.40)を使用できます。

以下に理由を書きますが、非常にしょうもないことです。

package.jsonのミス

nehan(<=6.0.38)においては、package.jsontypesという属性の値が./dist/indx.d.tsとなっていました。

しかし実はこれ正しくは./dist/index.d.tsと書かねばならなかったのです(eの字が抜けていた)。

- "types": "./dist/indx.d.ts",  // version 6.0.38
+ "types": "./dist/index.d.ts", // version 6.0.40

先に「Angular8以下では、nehan6.0.38を使ってください」と書きましたが、つまりそれは、このバグったpackage.jsonを使ったversion6.0.38を使ってください、ということです。なぜか?

理由は「そのバグがあるために、Typescript3.5以下でもエラーが出ないから」です。つまりバグのある古いバージョンを使うことで、Typescript3.5以下しか使えないAngular(<=8)でバグがでない、ということです。

実はnehanは各所にgetterと言われる機能を使っているのですが、実はこれ、Typescript3.6.3以降じゃないと次のようなエラーが出て、正しく使えなかった機能だったのです。

error TS1086: An accessor cannot be declared in an ambient context.

でもnehan6.0.38は正しいindex.d.tsが読み込めていない状態だったので、幸運にも? Typescript(<=3.5)でもこのエラーが出なかったのです!

というわけで、Typescript3.5.3までしか使えないAngular8では、nehan(>=6.0.40)を使用できません…

ちなみにAngular9になると、Typescript3.6系列がサポートされるらしいので、そこからはnehanを6.0.40にアップデートできます。

Angular + Ngrx所感

先月から試験的にAngular+Ngrxを使って縦書き文庫を作り直してみる、ということをしていたんですけど、パソコンが壊れバックアップも取ってなかったので、全て消え去りました(この記事はChromebookで書いてます)。

真面目にやると一年ぐらいかかる作業なので、一ヶ月程度の作業で済んだのは不幸中の幸い?なんでしょうかね。。。

Angular+Ngrxの感想としては、コツを掴むまでかなり時間がかかるということと、fluxなんでaction/reducer/state/effectの開発ループを回すのが面倒くさいなあ、ということです。

正直なところ「このまま続けるべきか?」と思ってたところなんで、もしかしたら中止できてよかったのかも。。。

学習コスト

TypescriptとRxに関しては多少ですが経験があったのでなんとかなりそうだったのですが、それ以外に覚えないといけないことが沢山あって大変でした。

よくAngular+Ngrxは大規模なチーム開発に最適などと言われますが、学習コストを嫌ってVueという選択をする人が多いのも、なんとなくわかるような気がします。

Ngrxについて

Ngrxを採用すると、とにかくRxでEffectsを書きまくることになるのですが、ここで割とわかりにくい地雷を踏みまくってしまいました。

特に危険なのはストリームを止めちゃう位置にcatchErrorを書いちゃったり、逆にストリームを止めるべきところでdispatch:falseを指定しないで無限ループを仕込んじゃったりとかです。

地雷1 catchErrorの場所

まずEffects中のcatchErrorですが、actions$直下のpipeではなく、map中のpipeで宣言しないといけません。

@Effects()
getFoo$ = this.actions$.pipe(
  ofType("foo"),
  switchMap(action => 
    return fetchFoo(action.payload.id).pipe(
      map(result => new GetFooDone(result.body)),
      // エラーは内側のpipeで補足する
      catchError(error => of(new GetFooFail({error})));
    );
  ),
  // ここに書いてはいけない
  // catchError(error => new GetFooFail({error}))
);

上の例は、fooに対応するactionを直下のactions$からフィルタリングし、そのデータを元にfetchFooでサーバーから何かしらのデータを取得しようとするコードです。

この処理の失敗を、action$直下のcatchErrorで補足すると、fetchFooでエラーが起きたときだけじゃなく、大本のactions$で起きたエラーも補足してしまいます。

上の例だと、GetFooFailというアクションを出したいのは、fetchFooが失敗したときだけですよね。

気をつけましょう。

地雷2 別のアクションに変化しない副作用

Effectsが新しいActionを返さないのにdispatch:falseをデコレーターに設定しないと、永遠にその処理が繰り返されます。

@Effect({dispatch: false})
logFoo$ = this.actions$.pipe(
  ofType("foo"),
  tap(_ => console.log("foo"))
);

上の処理で{dispatch: false}を忘れると、無限ループです。

自分はCPUがうるさくてパソコンが熱くなったので気づきました。自動ビルドで開発してたのですが、開いてる先のタブがいつのまにかCPU使用率100%になっていたりとか。

これがパソコンの熱暴走を起こし、寿命を縮めたんじゃないのか、と今では疑っています。

地雷3 Angular Materialとの相性

ngrx/storeの状態をangular materialのコンポーネントのディレクティブにダイレクトに反映させてしまうと、アニメーションが効かなかったり(例えばチェックボックスとか)、一部のUI要素が正しく計算できなくて表示できなくなったり(例えばタブコンポーネントの下線)みたいなことがあります。

なのでマテリアルコンポーネントを使用するときは、コンテナーというUIクッションを用意して、マテリアル要素はその下に置くようにします。

で、マテリアルなコンポーネントを含む下層のコンポーネントには「状態のコピー」を渡し、ローカルでその状態を操作させるようにします。そうしないと、アニメーションが正しく機能しないからです。その上で、子供の最新状態は@Outputを使ってコンテナーに吸い上げます。

つまり一元管理された状態を使用して制御できるのはコンテナーUIまでで、そこから下は状態のコピーが作成されることになります。全てが理想通り、とはいかないですね。

余談1

ちなみに壊れたMacbookの修理はざっくり7万ぐらいとか言われたんですけど、まあ買い直すよりかは、と思って依頼しちゃいました。

でも年末で忙しいから14営業日かかるとのこと。。。今からだと来年の10日前後?

余談2

そういえば最近、中国の開発者が「Immutableなんてほとんどのケースで必要ない。本当の未来はshadow-domを使い、素直にmutableな構造で、かつVueっぽいものだ」みたいなコンセプトでなんかフレームワーク作ってて、すごいスターをもらってた記憶があるのですが、名前を忘れました。

Typescriptのアレコレ覚え書き

書いておかないと忘れちゃいそうなので、残しておきます(たまに追記するかも)。

戻り値がbooleanであると同時に、引数が特定の型であることをコンパイラに教えることができる

これが便利なのは、こういう感じのコードにおいてです。

class Foo {
  say(){
    console.log("foo");
  }
}

let isFoo = (val: any): boolean => {
  return val instanceof Foo;
}

このisFooを使って、ある値の型がFooである場合に(Fooのメソッドである)sayを呼ぶ、といった処理を書きたくて

let func = (val: any) => {
  if(isFoo(val)){
    val.say(); // val は any なのでエラー
  }
}

などとやると、コンパイル・エラーが出ます。上のコードでは(当たり前ですが)valの型がanyとみなされるからです。

しかしこれでは、せっかくFooであることを確認するisFoo関数の意味がなくなってしまいます。

しかしこの問題は、isFooを、次のように書き換えることで解決します。

let isFooEx(val: any): val is Foo {
  return val instanceof Foo;
}

戻り値の型の部分に記述されたval is Foo の働きによって、引数のvalの型は呼び出し元にFooだよ、と認識され、次のコードが通るようになります。

let func(val: any){
  if(isFooEx(val)){
    val.say(); //  val は Foo なのでOK!
  }
}

このval is Fooの部分のことを、Type Guard と呼ぶのだそうです。

詳しくは、Type Guards and Differentiating Types を参照して下さい。

型引数にもデフォルト値を設定できる

TypeScript 2.3からですが、次のような感じで型パラメータにデフォルト値を設定できるようです。

class Component<Props = any, State = any> {
  props: Props;
  state: State;
}

type StateActionPair<T, V extends Action = Action> = {
  state: T | undefined;
  action?: V;
};

二番目の例にあるV extends Action = Action の部分は、Actionの部分型であるVのデフォルト型はActionである、という意味です。

これまでこういったことを知らなくて、色々と汚いコードを書いてました…反省。

Nullチェックを無視させることができる

nullが入り得る型なんだけど、そのチェックがいらないことが明らかな場合、コンパイラnullチェックを無視させることができます。

let foo = (entity?: Entity): string {
  return entity!.name;
}

entity!という部分がそれです。

上のプログラムにおいてentityという引数は、Entity型もしくはnullもしくはundefinedになる恐れのある引数だ、と言ってるので、普通にentity.nameと書くと、コンパイルエラーになります(ただし--strictNullChecksが有効時)。

しかしこういうnullなどになり得る変数のあとに!をつけると「ここは特別にnullにならないからチェックしなくて良いよ!」という意味になります。

これも使えると時々は便利です。

参考:Non null assertion operator

undefinedならプロパティアクセスさせず全体をundefinedにする演算子

例えばこんなオブジェクトがあるとして

const obj: any = {
  foo: {
    bar: 'bar'
  }
};

次のようにアクセスするとエラーになります。

const value = obj.bar.bar //  Cannot read property 'bar' of undefined

こういう木構造の途中がundefinedかもしれないものにアクセスするときに、いちいち

const value = obj.bar? obj.bar.bar : undefined;

みたいに書くのは面倒ですよね。しかし?演算子を使うと、これを

const value = obj.bar?.bar;

というように、短く書くことができます。

仮にobj.barundefinedでも、obj.bar?で評価が止まり、全体がundefinedになるわけですね。

switchが買えないらしいので、どのぐらい買えないのか試してみました

マリオ発売の直前に変えたら凄い、と思ってチャレンジしてみました。

まずは値動きをチェックする巡回プログラムを書きます。

#!/usr/bin/python3
# -*- coding: utf-8 -*-
from pyquery import PyQuery as pq
import os

def notify(subject, message, email):
    os.system("echo '{message}' | /bin/mailx -s '{subject}' {email}".format(message=message, subject=subject, email=email))

def parse_price(doc):
    price_tag = doc.find("#priceblock_ourprice")
    price_text = price_tag.text().replace("¥ ", "").replace(",", "");
    return int(price_text)

def check_price(url):
    doc = pq(url)
    price = parse_price(doc)
    subject = "switch price - " + str(price)
    message = url
    email = "foo@example.com"
    if price <= 33000:
        notify(subject, message, email)

if __name__ == '__main__':
    urls = [
        "https://www.amazon.co.jp/dp/B01NCXFWIZ", 
        "https://www.amazon.co.jp/dp/B01N5QLLT3"
    ]
    for url in urls:
        check_price(url)

正規価格で売られていたらメールする」というだけのプログラムです。

これを適当なレンタルサーバーで、2分おきに実行させます。

crontab -e

2/* * * * * /usr/local/bin/chkswitch.py

最後に、今まで一度も使ったことのないamazonの「1click注文」を有効にしました。

「カートに入れる」「配送方法の選択」「購入」なんてことをやってたら、その間に売り切れちゃう気がしたので…

結果

さっそく今日、5回ほどメールが届いて、即座に1clickで購入を試みてみましたが…

結果は全敗でした!

この程度の工夫では買わせてくれないみたいです。

とりあえず知見

  • 1click購入は必須だと思います(わざわざカートに入れてたら間に合わない)
  • 巡回頻度は1分ですら遅いのかも?
  • 木曜日と金曜日が狙い目と聞いてましたが、木曜日は確かにヒットしました

追記

なんか11月頃から普通に定価(3万2000円ぐらい)で買える感じになってます(自分も無事に定価で買うことができました)。

クロスドメインで子フレームから親ドキュメントのwidth:100%を取得する方法

ひょんなことからviewportdevice-widthbodyタグから適用される事を知りました。

別の言い方をすれば「bodyより上位のタグには適用されない」ということみたいです。

例えば次のようなページ(parent.html)をiPhone5device-width = 320px)で開き、iframeを使ってchild.htmlを読み込ませます。

<!-- parent.html -->
<html>
  <head>
    <meta viewport content="max-width=device-width">
  </head>
  <body style="margin:10px">
    <iframe width="100%" src="https://other-domain.com/child.html"></iframe>
  </body>
</html>

<!-- child.html -->
<html>
  <head>
    <meta viewport content="max-width=device-width">
  </head>
  <body>
    this is child!
  </body>
</html>

このときchild.htmlで、bodyhtmlの横幅をそれぞれ確認すると…

$("html").width(); // 300px
$("body").width(); // 320px(device-width)

htmlの横幅 < bodyの横幅

という結果になりました。

つまりchild.htmlのbodyにはdevice-widthが設定されていますが、htmlには親(parent.html)のコンテキストを考慮したサイズ(width:100%)が生きているのです。

クロスドメインだと子フレームから親ドキュメントの情報を取得する処理は(セキュリティー上の理由で)ブロックされますが、この情報を使えば少なくとも親ドキュメントの横幅は取得できる、ということになります。

もちろんchild.htmlmetamax-width=device-widthが設定されている必要がありますが。

Googleの検索結果でリダイレクトとトラッキングを無効化する

Googleの検索結果からリンクをコピーして貼り付けたら、めちゃくちゃ長いリダイレクト用のリンクになっていて「ふぁ??」となることがあります。

なので、それをなんとかしてくれる拡張機能を探したのですが、しっくり来るのがなかったので自作することに。。。

拡張機能のソース

// manifest.json
{
  "name": "Disable google search tracking",
  "version": "0.1",
  "manifest_version": 2,
  "description": "Disable google search tracking",
  "permissions":[
    "tabs", "http://www.google.co.jp/*", "https://www.google.co.jp/*"
  ],
  "content_scripts": [
    {
      "matches": ["http://www.google.co.jp/*", "https://www.google.co.jp/*"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ]
}

// content.js
(function(){
  setTimeout(function(){
    document.querySelectorAll("h3.r>a").forEach(function(link){
      var a = document.createElement("a");
      a.href = link.href;
      a.innerHTML = link.innerHTML;
      link.innerHTML = "";
      link.appendChild(a);
    });
  }, 100);
})();

仕組み

リンクエレメントを直接どうこうしようとしても、Google側のjsで強引に書き換えられてしまうので、リンクエレメントの子に非Redirectなリンクを挿し込む方法を取りました。

こうすれば、リンクのクリックイベントは最深の子のみで発火するので、親リンクのRedirectとトラッキング用のイベントハンドラは無効化されます。

これで純粋なリンクのみがリストアップされたクリーンな検索結果になりました。