anti scroll

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

HomebrewからMacPortsへの移行でハマった部分(主にRust周り)

2023年の中旬ごろ、HomebrewがBigSurなどの古いMacOSをサポートから外してしまいました。

しばらくは「なんとかなるさ!」と使っていたのですが、半年もしないうちに重要なパッケージが更新できなくなり…

特に痛かったのは、libheifが更新できなくなって、ImageMagicでavifを扱えなくなったことでした。

そこで古いOSもサポートしていると噂のMacPortsに移行することに。

他の移行先としてnixなども考えましたが、ちょっと調べた感じだと「まだ過渡期なのかな」と思ったので、無難にMacPortsにしました。

で、実際に使ってみたら、拍子抜けするぐらい、普通に移行できました。

しかし、やはり一部ハマったものもあったので、その経験を残しておきます。

ハマりポイントは2つで、自分の場合はcargo-edit(Rust)とfabric(Python3)でした。

cargo-editのインストール(と起動)に失敗する

MacPortsを運用してしばらくして、Rustのcargo-editというcrateのインストールに失敗することに気付きました。

エラーメッセージを見ると、libiconv.dyldという動的ライブラリのリンクに失敗している模様です。

で、どうもそのリンク先が、MacPortsが管理するlibiconvっぽい…

MacPortsでlibiconvをインストールした覚えはないのですが、広く使われてる文字列変換のライブラリなので、まあなにかのついでにインストールされたのでしょう。それがリンクされて、エラーになったみたいです。

エラーになる理由は、MacPortsでインストールされるlibiconvが、世間一般に公開されている「普通の」libiconvとは、ちょっとだけ異なるから。

なんか他のエコシステムのlibiconvと衝突しないように、いくつかの関数が別名で登録されているらしいのですね。

だからこれをリンク先の動的ライブラリとして選択されてしまうと、リンクエラーになってしまうわけです。

なにが異なるのかは、

nm -a /opt/local/lib/libiconv.dylib | grep iconv
nm -a /usr/local/lib/libiconv.dylib | grep iconv

の出力結果を比べたらわかるはずですが、実際にやってみたら「アレ? 別に変わったところはないけど?」という感じでした。

でもまあ、リンクエラーが起きてるんで、なんか違うんでしょう(笑)。

そういうわけで、MacPorts外の世界のプログラムにlibiconvをリンクするときは、MacPortsが使っている(特別仕様の)libiconvをリンクさせないようにする必要があります。

今回トラブったcargo-editについても、DYLD_LIBRARY_PATHが最初に/opt/local/libを探す仕様になっているせいで、そのままコンパイルするとリンクエラーになるわけです。とはいえ、DYLD_LIBRARY_PATHの上書きは、他のトラブルを起こすことが多いので避けたいところです。

これを解決する方法として、自分の環境では次の2つがうまくいきました。

1つ目は、Rust固有の環境変数を使うもの。

幸いなことに、RustにはRUSTFLAGSという環境変数を通じて、コンパイル時のオプションを渡せる仕組みが用意されています。これを使ってリンク先を変更することができました。

.zshrcとかに、

export RUSTFLAGS="-L/usr/local/lib"

を付け足せばOKです。

もちろんBigSurでは/usr/local/libにlibiconvは入っていないので、ソースからコンパイルして、/usr/local/lib/にインストールしました。

./configure --prefix=/usr/local && make && make install

2つ目の方法はもっと簡単で、cargo-editをインストールするときだけMacPortsのlibiconvを無効にする、という方法。

# いったん無効にする
sudo port -f deactivate libiconv
# cargo-editをインストール(システム依存のlibiconvが使われる)
cargo install cargo-edit
# 終わったら再び有効にする
sudo port activate libiconv

こうすると、MacPorts以外のlibiconvにリンクしてくれます。

自前で/usr/local/libなどにインストールしてない状態でもうまくいったので、BigSurのどっかしらに、普通のlibiconvがあるんでしょう(笑)。

これでcargo-editはインストールできました。

ただ自分の環境では、これで終わりではなく、その後にcargo-edit系のコマンド(upgrade, add, rm, set-version)を実行したら、また次のようなエラーが出ました。

~/.cargo/registry/index/github.com-1ecc6299db9ec823 is unusable due to having an invalid HEAD reference: reference 'refs/heads/main' not found; class=Reference (4); code=NotFound (-3)

なんかcargo-editに関するgithubのレポジトリ情報が~/.cargo以下に適切に配置されていない、みたいなエラーです。

最近のcargoは、パッケージを管理するcrates-ioというサーバーとの通信に、sparseというプロトコルを使うようになったらしいのですが、cargo-editをインストールする際にもこのプロトコルが使われたことで、githubのrepositoryが~/.cargo/registry以下に適切に登録されなかったっぽいです。

以前のcargoは、巨大なリポジトリをgitプロトコルでローカルに引っ張ってくる仕組みで、その際レポジトリの構造がローカル環境にも配置されていたのでしょう。

しかしcargoのプロトコルの刷新で、それがなくなったことにより、このエラーが出たのだと思います。

エラーを見る限りcargo-editはローカルにgit-repositoryが配置されてないと動かないらしいので、最初にcargo install cargo-editするときだけは、gitプロトコルcrates-ioと通信するようにします。

これも2つの解決方法があって、1つはCargo.tomlに

[registries.crates-io]
protocol = "git"
#protocol = "sparse"

と書く方法。もちろん終わったら"sparse"側を有効にします。

もう一つは環境変数CARGO_REGISTRIES_CRATES_IO_PROTOCOLをセットする方法で、.zshrcなどに

export CARGO_REGISTRIES_CRATES_IO_PROTOCOL="git"

と書いておきます。

こうした上で、cargo uninstall cargo-editしてから、cargo install cargo-editすると、正常にインストール&起動するようになりました。

MacPorts経由のfabricが動かない

cargo-editほど大変ではありませんでしたが、fabricもすんなりとは動きませんでした。

最初はMacPortsfabricで検索したら、pythonのそれぞれのバージョンごとに用意されていることがわかったので、素直にそれをインストールしました。

しかし実際に使ってみると、いくつかの処理がエラーで動きませんでした。

そこでpython3pip3MacPortsでインストールし、fabricだけpip3でインストールするようにしたら上手くいきました。原因はわかりません(笑)。

ちなみにMacPorts経由でインストールしたpipでインストールするfabコマンドは、/opt/local/Library/Frameworks/Python.framework/Versions/3.xx/bin/にインストールされるので、sudoで実行する必要があります。本当はvenvとか使ったほうが良いんでしょうが、面倒なのでスキップしました。

# python3とpip3をインストール
sudo port install py311 pip-3.11

# pip, pip3ともにpip-3.11を使用
sudo port select --set pip pip-3.11
sudo port select --set pip3 pip-3.11

# Macportsのpipでfabricをインストール(sudoに注意)
sudo pip install fabric

だれでも簡単にページ組版が扱えるウェブ・コンポーネント「nehan-player」を公開しました

nehanによるページ組版を簡単に扱えるウェブコンポーネントを作ってみました。

導入

テキストの内容を、サーバーからsrc属性で読み込む場合は、こんな感じになります。

<nehan-player writing-mode="horizontal-tb" width="responsive" src="/path/to/your-novel.html" height="500" font-size="16" layout="1x2">
</nehan-player>

<script src="https://cdn.jsdelivr.net/npm/nehan-player@0.0.2/dist/nehan-player.min.js"></script>
<script>
  NehanPlayer.initialize(); // 初期設定(オプションなし)
</script>

タグの中にそのまま内容を記述したい場合は、<div slot="content"> ~ </div>の中に本文を記述します。

<nehan-player writing-mode="horizontal-tb" width="responsive" height="500" font-size="16" layout="1x2">
  <div slot="content">
    <h1>はじめてのnehan-player</h1>
    <p>はじめてのテキスト</p>
  </div>
</nehan-player>
  
<script src="https://cdn.jsdelivr.net/npm/nehan-player@0.0.2/dist/nehan-player.min.js"></script>
<script>
  NehanPlayer.initialize(); // 初期設定(オプションなし)
</script>

ブログやホームページでも、本文にHTMLをそのまま打ち込める環境なら、問題なく導入できると思います。

はてなブログの場合は、管理画面から「デザイン設定」→「カスタマイズ」→「フッター」と進んで、<script> ~ </script>の部分をコピペしておくと、<nehan-player> ~ </nehan-player>の部分を入力するだけでOKです。ただHTMLをそのまま入力できるのはProプランのユーザーのみらしいですので、無料ユーザーはnehan-playerを記事中では使えないということになりますね…

ちなみにウェブコンポーネントということで、親ページからの影響を受けませんし、親ページに影響を与えることもありません。

完全に独立した表示要素として、安心して導入できます。

またレスポンシブにも対応しているので(nehan-playerタグのwidth属性を"responsive"にするだけ)、スマホ対応も簡単です。

なおnpmから使うときは、

npm install nehan-player --save

のようにインストールし、

import { NehanPlayer } from 'nehan-player';

NehanPlayer.initialize();  // 初期設定で使う(オプションなし)

で、呼び出せます。

initializeに与えるオプションついてはドキュメントを参照してください。

デモ

さて、ここで、はてなブログにウェブコンポーネントを貼り付けたかったのですが、どうやらHTMLをそのまま記述できるモードは、現在Proユーザーに限定されているようです。

なので、ちょっと裏技的ですが、はてなブログaboutページ(HTMLを入力できる)に、nehan-playerを貼り付けて見ました。

ひと手間が必要な場合

1. 配色を変えたい

一応、nehan-playerのページに、こちらで用意したテーマがありますので、利用するなり、編集するなりしてください。

使い方は、nehan-playerタグのtheme属性に、テーマファイルへのパスを記述するだけです。

<nehan-player (省略) theme="/path/to/your-theme.css"></nehan-player>

ただし、公式のテーマはどれも内部で、default-layout.cssっていう別のファイルを読み込んでいるので、テーマファイル単体だけじゃなく、default-layout.cssも忘れずにサーバーに設置してください。

2. 画面の左右をクリックしたらページ送りしたい

initializeを以下のように呼び出すと、画面の左右クリックでそれぞれのページに移動するようになります。

NehanPlayer.initialize({
  onClickLeftPage(player){
     player.gotoLeftPage();
  },
  onClickRightPage(player){
     player.gotoRightPage();
  }
});

3. 読み込んだテキストを、ちょっと編集したい

initializeonFetchContentを使います。

NehanPlayer.initialize({
     onFetchContent(src: string, content: string){
         if (src.endsWith(".txt")) {
            return content.replace(/^\n+/, "").replace(/\n/g, "<br>");
         }
         return content;
     }
});

4. Androidで縦書きを扱いたい

これはちょっと少し工夫が必要です。

というのも、Androidには縦書きに対応したフォントが入ってないこともあり、その場合は自分で提供しないといけないからです。

やることは2つで、

  • 縦書きに対応したフォント(例えばIPA明朝とか)をサーバーに置く
  • それを@font-faceで読み込むcssを「headタグ」に入れる(ウェブコンポーネントなのに)

です。

ちなみに2つ目で「cssをheadタグに入れる(ウェブコンポーネントなのに)」と書きましたが、どういうことでしょうか。

実はこのcss、本当はウェブコンポーネントの「内部」に入れないといけないのです(じゃないと外部のページに影響を与えないというウェブコンポーネントの趣旨と矛盾するから)。

が、どうも現状のブラウザは、どれもshadom DOMから@font-faceが読み込めないようなのですね。

上のリンクは2014年の議論ですが、どうやら技術的に、とても難しいことらしいのです。

で「ものすごい強い要請があるまでは、このままにしとこうや」って感じになってるみたいでして…

というわけで、これを回避するため、@font-faceを指定したファイルは、普通にHTMLのヘッダに読み込む必要があります。ようするに、

  • @font-faceを含まない普通のcss(テーマとか)は、ウェブコンポーネントの内部に読み込まないといけない(まあそうじゃないとウェブコンポーネントにする意味がないので、当たり前ですが)
  • けど、@font-faceを含むcssは例外。shadow DOMの外側(shadow host側って言うそうですが)じゃないと読み込めない。

ということです。

ゲームパッドの操作を記述する言語「combo-script」と、その再生アプリ「combo-player」を作りました

きっかけ

作ったきっかけは、最近買ったウイイレ2020の操作が、ちっとも頭に入らなかったからです。。。

一応ゲーム内にマニュアルがあるんですけど、文字とか矢印で説明されても、ピンと来ないんですよね。。。

そこで思いついたのが「ゲームの操作をぱぱっと記述したら、パッド上の動きをアニメーションを再生しつつ、操作ログも出してくれる」みたいなものがあったら、理解が捗るかなあ〜?ということです。

というわけで作ったのが、combo-scriptと、combo-playerです。

例えばcombo-scriptで昇龍拳を記述して、combo-playerで再生するとこんな感じになります。

f:id:convertical:20200811165349g:plain
昇龍拳

その他、色々なコマンドについては、デモサイトを見てください。

tategakibunko.github.io

上のデモサイトでは、ウイイレの他に、スト5とか鉄拳のコマンドとかも追加しておきました。

メニューの「Games」から選択できます。

コマンド部分は編集可能になっているので、色々と書き換えて遊んでみてください。

f:id:convertical:20200811165922p:plain
コマンド部分(編集可能)

combo-script

combo-scriptは、ゲーム操作を記述するための言語です。こんな風に記述します。

コナミコマンド

up, up, down, down, left, right, left, right, A, B

昇龍拳十字キー操作で記述)

right, down, (right, down), punch

(right, down)は右と左を同時押し(つまり右下)という意味です。

一般にボタンの同時押しは(X, Y, Z...)のように記述します。

昇龍拳(スティック操作で記述)

setL(0), moveL(0, 90), rotateL(90, 45), punch

これは左スティックを0度(真右)に方向け、そこから真下に動かし、更にそこから45度(右斜め下)の位置まで回転させ、最後にパンチボタンを押す、という意味です。

setL, moveL, rotateLの引数は、傾ける先の角度ですが、この角度は時計回りであることに注意してください。時計回りなので、90度は真上ではなく、真下です。

ウイイレ、コントロールシュート

R2 { square }

上の構文はHolding Syntaxと言って、X { Y, Z, ... } などと書くと「Xの操作をしながらY,Z」という意味になります。

なので上のコマンドは

  • R2を押しながら□ボタン
  • 終わったらR2を離す

という意味になります。

combo-script(言語仕様)

詳しくは公式サイトを見てほしいのですが、大雑把に説明すると、

  • ボタンの押下、押しっぱなし、同時押し、各種スティック操作などを抽象化した言語です。
  • 特別なプログラミング知識がいらないので、誰でもコマンド部分を記述できます。
  • 矛盾した操作はコンパイル時にエラーが出ます(押し込んでいる最中のボタンを再び押すとか、左と右を同時に押すとか、色々)。
  • 押下についてはpushDown, pushUpを個別に扱う方法もあります。
  • プラグインで拡張することができます。
  • 定義済みの関数(rotateLとかmoveLとか色々)以外のシンボル名は、ユーザー定義のボタン名、もしくはユーザー定義のプラグイン関数として解釈されます。
  • 例えばpunchと書いたら、ユーザー定義のpunchボタンがあって、それを押して離す、という一連の動作として解釈されます。つまり、punchという名前のボタンをどう解釈するかは、combo-scriptを組み込む実装者に任されます。
  • 関数も同様です。例えばfoo(1,2)と書いた場合、そういう定義済みの関数はないので、ユーザー定義のプラグイン関数とみなされます。そして、実際の動作はcombo-scriptを組み込む実装者に任されます。
  • つまりボタン名も関数名も、それを解釈するのはプレイヤー側の仕事で、combo-script側は名前にルールや制限を設けません。コンパイラ側が判断するのは、それがボタンか、関数か、そしてそれらの動作は、どういう文脈で押されたか、ということだけです(通常押し、同時押し、押しっぱなしなど)。
  • というわけで、例えばパッド右側にある4つのボタンなどは、プレステ風にtriangle/circle/cross/squareと定義してもいいし、Xbox風にY,B,A,Xと定義してもいいです。
  • ただしボタン名を数字から始めることはできません。つまりbutton1はいいけど、1buttonは駄目。
  • ちなみに、combo-scriptは大文字と小文字を区別しません。だからupと書いてもUPと書いても同じです。これは関数も同じ。
  • あとボタン名については英数じゃなくてもいいです。例えば日本語で強パンチとか書いても問題ありません。

コンパイルの仕方とか、それをどうやって自分のプレイヤーアプリに組み込むか、みたいなことは公式サイトを確認してください。

感想

  • ウイイレ2020は難しい。
  • 昇龍拳のコマンドを初めて知った。
  • 鉄拳のコマンドを思い出すことが出来た。

AngularのExpressionChangedAfterItHasBeenCheckedErrorに関する覚え書き

ネットでExpressionChangedAfterItHasBeenCheckedErrorを検索すると、よく「子が親の状態を変更するとビューに一貫性がなくなることが警告される」という説明を目にしますが、少し混乱を招きかねない説明なんじゃないかなあと思いました。

正確には「Angularが変更検知をする際に行う探索順において、後からチェックされるビューから、先にチェックされるビューの状態を変える処理を書くと警告される」と説明すべきなのではないでしょうか。

なぜなら、HTMLには親子関係だけではなく、隣接(兄弟)関係というものがあるからです。親が子より先に探索されるのは当たり前ですが、隣接関係については自明ではありません。

例えば次のようなツリーがあったとします。

A, B
|
C

AとBは隣接(sibling)関係で、Cの親はAです。このときCの親であるAがCより先に探索されることは自明ですが、Bはどうでしょうか。Cより先でしょうか、後でしょうか。

もしAngularの変更検知が「幅優先探索」の順で行われるなら、Bが先です。しかし実際のAngularの変更検知順は「深さ優先探索」なので、検知される順に番号をふると、次のようになります。

A(1), B(3)
|
C(2)

つまりBは、Cよりも「後に」変更検知されるわけです。

だから探索順に置いて最後になるBにおいて、Cが依存するような状態を変更すると(先に確定されたCの状態を後からBが変えることになるので)ExpressionChangedAfterItHasBeenCheckedErrorというやつが出ます(ただし警告してくれるのは開発ビルドのときだけ)。

よくウェブで見る「親と子」という説明の仕方だと、幅優先か深さ優先かで受け取る人の解釈が曖昧になるので「探索順」を使った説明をしたほうが親切なのではないでしょうか。

Time-based renderer task throttling

Chrome ver56から導入されるようですが、ざっと読んだ感じだと、こういう動作らしいです。

  • バックグラウンドタブの処理に時間枠を設ける。
  • バックグラウンドタブの処理時間が時間枠を超えると、値が負になって(いったん)眠る。
  • ただし眠りっぱなしというわけではなく、1秒ごとに0.1secずつ復活する。

これによって、モバイル用ブラウザのバッテリー節約につながるとのこと。

Time-based renderer task throttling

一方、バックグラウンドでアレコレ(データを同期するとか、ストリームを読むとか)してるサイトは、軒並み影響されるだろうという意見もあります。

Chrome 56 Will Aggressively Throttle Background Tabs

ちなみに縦書き文庫に関しては、あんまり影響はなさそうです。

どうせ別タブで開かれたら再描画回数が抑えられるので、requestAnimationFrameがゆっくりとしか周りません。結果としてnehan.jsの組版も、ゆっくりとしか進まなくなります。

修正:2011年の時点から、ゆっくりどころか、全く周らないようになってるそうです。 https://developers.google.com/web/updates/2017/03/background_tabs

というわけで、この背景処理が仮にthrottlingで抑えられたところで、タブがアクティブじゃなきゃ、どのみち大したスピードは出ないわけで…これまでと同じですね。

数年前まではどのブラウザでも「おっきな作品を別タブで開いておいて、組版が終わるまで別サイトを眺めてる」なんて使い方ができたわけですが…

textNodeの出力オプションを追加

2chからのアクセスに、珍しくリファラが付いてたので、元スレを覗いてみたのですが、縦書き文庫に投稿された作品へのリンクに「踏まないほうがいいよね?」ってレスが付いていて、地味に傷ついたのでした。

capturePageTextオプション

それはさておき、表題の件です。

まだリリースはされていませんが、nehan.jsのリポジトリ上では、 Nehan.PageStreamsetContentや、 Nehan.PageStreamasyncGetという関数のオプションに、capturePageTextという新しいオプションが追加されました。

これを有効にすると、出力されるページに、DOMElementで言うところのtextNode的な文字列が入ってきます。

var pe = Nehan.createPagedElement().setContent("<p>foo</p>", {
  capturePageText:true, // textNodeを出力する
  onProgress:function(tree, ctx){
    console.log(tree.text); // "foo"
  }
});

この機能を使えば、例えばビューアーに検索機能を付けたい場合、filter関数を使って、

var search_pages = function(pe, keyword){
  return pe.filter(function(page){
    return page.text.indexOf(keyword) >= 0;
  });
};

var cool_pages = search_pages(pe, "cool!");

とかやれば、キーワードにマッチするページ配列が取得できるようになります。

Nehan.BoxとNehan.Pageの違い

ちょっとした捕捉ですが、filterが返すのは、Nehan.Pageの配列ではなく、Nehan.Boxの配列であることに注意して下さい。

つまり、上のプログラムにおけるcool_pagesのそれぞれには、elementメンバが存在しません。

console.log(typeof cool_pages[0]); // "Box"
console.log(typeof cool_pages[0].element); // "undefined"

これはなぜかというと、nehan.jsが計算するページは、実際にそのページが画面に表示されるまでは論理レイアウトNehan.Box)として保存されているからです。

nehan.jsでは、画面に表示されるときになって初めて、論理レイアウト(Nehan.Box)がon the flyで実レイアウト(Nehan.Page)に変換されます(これはたかだか1ページぶんの変換なので、一瞬で終わります。もちろん評価が済んだページはキャッシュされるので、再計算も発生しません)。こうやって生DOMの生成を遅延することで、全体のパーススピードを上げているわけです。

まだ表示されていないページを検索結果に含めるために、filter関数の戻り値はNehan.Boxの配列となっています。

余談

最後にnehan.jsの検索性について、ちょっと余談です。

よくnehan.jsのbrタグについて「CTRL+Fの検索が使えない」と言う人がいますが、段組みで表示する場合はその通りとして、ページ送りの組版にとってはどのみちCTRL+Fはあまり意味がないことに留意していただきたく。

例えば、検索ワードが最終ページにヒットするケースを考えて下さい。

そういう場合、いま見ているページをすっ飛ばして、いきなり最終ページにジャンプするわけにはいかないじゃないですか。

だから、ページ送りで表示するドキュメントの場合、どのみち検索機能は別のUIを用意する必要があるわけです。

だからbrはたいして問題じゃないように思うわけですが、どうなんでしょうね?

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

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

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

使ってみた感想

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

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

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

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

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

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

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

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

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

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

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

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