タッチパッド機におけるブラウザのJavaScriptの、エミュレートされたマウスイベントと、インタラクティブなコンテンツとの干渉について

サンプルを https://github.com/metanest/mobileBrowserMouseEmulationInteraction に置いてあります。

仕様としては、W3Cによるタッチイベントの仕様 https://www.w3.org/TR/touch-events/#mouse-events に含まれているのですが、タッチパネル操作のスマートフォンタブレットのウェブブラウザのJavaScript では、タッチ操作のイベントの後に、(既存アプリ用の)マウス関係のエミュレートされたイベントを発生させることが、仕様で考慮されており、多くの実装がそのような挙動をします。

仕様中に、

If the contents of the document have changed during processing of the touch events, then the user agent may dispatch the mouse events to a different target than the touch events.

とあるように、先に発生したタッチイベントでコンテンツを書き換えた場合などは、同じ場所で後から発生する(マウス)イベントは、同じ場所に新しくできた違う対象に対して発行されるかもしれない、ということになっています。

これは実際のところ、厄介な挙動になり得ます。サンプルは、あるエリアをクリックするとオーバーレイが表示され、そのオーバーレイをクリックするとオーバーレイが消える、というものですが、スマートフォンのブラウザに表示させて試してみると、オーバレイがすぐ消えてしまうような挙動をする(場合が多い)はずです。実際にどんなイベントが起きているかは JavaScript コンソールに出力しているので、そちらで確認できます。

なお、仕様では、

If the preventDefault method of touchstart or touchmove is called, the user agent should not dispatch any mouse event that would be a consequential result of the the prevented touch event.

となっているので、(あるべしとされている通り実装されていれば)それに従うことで抑止できるはずです。

特に、各種のフレームワークの内部でこれが起きている場合、把握するのはなかなかに厄介です。

例えば、こちら( https://github.com/callemall/material-ui-webpack-example/tree/master/src/app )にある(2018年2月末現在)Material-UI のサンプルでは、react-tap-event-plugin というプラグインを利用して onTouchTap というハンドラで処理をしています。そしてこのサンプルを、onTouchTap の利用をやめ onClick のみに変更されている、バージョン 0.19.0 及び、それよりも後の Material-UI で動かすと、開いたダイヤログの背景の onClick が呼ばれて、ダイヤログがすぐに消えてしまう、という現象が起きます。*1

JavaScriptのモジュールのimportの循環が必要な場合について

サンプルを https://github.com/metanest/jsMutuallyRecursive に置いてあります。

実行するには、package.json と package-lock.json を同梱してありますので*1、まず npm を次のように実行して必要なモジュールをインストールした後*2

$ npm install .  # 最後の . (ピリオド) を忘れないように

run_webpack.sh を実行すると、webpack が実行されて、ディレクトリ dist1/ と dist2/ の中に NodeJS で実行可能なスクリプトが生成されます。

用語の定義

以降の説明の都合で、JavaScriptスクリプトの実行の流れに関して、少し用語の定義をします。

次のようなスクリプトを実行すると、

console.log(1);
function f() {
  console.log(4);
}
console.log(2);
console.log(3); f(); console.log(5);
console.log(6);

1 から 6 まで順番にコンソールに出力されますが、このような場合の 4 以外の実行の流れを「上から下への実行」、4 の流れを「関数内の実行」と、以下では呼ぶことにします。

循環があっても、関連が疎であり、特に問題が無い場合

src1/ にソースが置いてあり dist1/ に生成されるサンプルは、循環があっても特に問題が無い場合です。

具体的には、mainが次のようで、

import isEven from './isEven';
import isOdd from './isOdd';
console.log(isEven(4));
console.log(isEven(5));
console.log(isOdd(6));
console.log(isOdd(7));

実行結果は次のようになります。

$ node dist1/main.js
This is in isOdd
undefined
========
This is in isEven
[Function]
========
true
false
false
true

また、それぞれの子モジュールの内容は次のようになっているのですが、このように「関数内の実行」からのみの参照であれば問題はありません。undefined が出力されている場所があるように、「上から下への実行」の途中では、import の結果が undefined になっている時はありますが、関数が実行される時には「中身が出来て」います。

// isEven.js
import isOdd from './isOdd';
console.log('This is in isEven');
console.log(isOdd);
console.log('========');
export default x => (x === 0) || isOdd(x-1);
// isOdd.js
import isEven from './isEven';
console.log('This is in isOdd');
console.log(isEven);
console.log('========');
export default x => ! ((x === 0) || ! isEven(x-1));

対称的になっている通り、mainにおける isEven と isOdd のインポートの順序も、どちらでも構いません。

関連が密で、循環による問題がある場合

src2/ にソースが置いてあり dist2/ に生成されるサンプルは、問題がある循環の場合です。

具体的には、mainが次のようで、

import yin from './yin';
import yang from './yang';
yang.yin = yin;
console.log('yin');
console.log(yin);
console.log('========');
console.log('yang');
console.log(yang);
console.log('========');

(このmainにある yang.yin = yin; という代入は、以下のような結果を得るために、どうしても必要)

実行結果は次のようになります。

$ node dist2/main.js
yin
{ yang: { yin: [Circular] } }
========
yang
{ yin: { yang: [Circular] } }
========

各モジュールは次のようになっています。

// yin.js
import yang from './yang';
export default { yang: yang };
// yang.js
import yin from './yin';
export default { yin: yin };

(yinとかyangというのは、古代中国思想の陰陽のことで、米国の古いハッカーが相互再帰的なものを説明する時にたまにこのような名前を使っているのを見ます)

main → yin → yang のようにインポートが進んで、そこからさらに → yin のインポートは循環として検出されて阻止されますから、yangの中での「上から下への実行」の途中では yin は undefined になっていて、先に示したようにmainのほうで補完してやっています(あれこれいじってみましたが、他に方法は無いようでした)。

「上から下への実行」の途中で実際に参照ができる必要がある、現実的なコードの例としては、classのextendsでの親クラスの指定などがあります。

サンプルで意図的にやっているような、双方向に直接の参照が必要な場合は、この例の yang.yin = yin; のように外部から操作して循環を完成させないといけません。一方、例えば親クラスと子クラスの関係のように*3、「上から下への実行」中の参照は片方向で循環しない場合は、この例の yin の側のように、mainで(上位側で)インポートする順番に注意する必要があり、直接に参照している側を先にインポートしてやればうまくいきます。

*1:package-lock.json は、内部に記述されている多数の依存先のバージョンのどれかに脆弱性があると GitHub から警告が来るので外しました。

*2:ピリオドを忘れると、npmの、ディレクトリを遡る特性で変なことになる場合があります。

*3:メソッド中などで必要なために、importは循環になっていても

NKF.guess はもう古い。国際化時代のコードセット(エンコーディング)推測法

柄にもなく釣りっぽいタイトルですが、本文中には特にそんな派手なものはありません。

はじめに

Rubyの多言語化に関してまず読むべき、るびま記事である「Ruby M17N の設計と実装」が2009年の記事ですから四捨五入すればもう、ひと昔前のことですが、それでもなかなか移行は進まないもののようなので(あと少々クセがある)、エンコーディングの推測についてまとめた記事を書いてみます。

エンコーディングの変換

エンコーディングの変換については、例の記事の「エンコーディングの変換」の節( http://magazine.rubyist.net/?0025-Ruby19_m17n#l76 )と、詳細は最新のマニュアルの String#encoding についてのページ( https://docs.ruby-lang.org/ja/latest/method/String/i/encode.html )から追ってください。

NKF.guess はもう古い?

確かに NKF.guess は便利です(便利でした)。しかし K がおおもとは「漢字」に由来していることが暗示するように、現代の状況への対応には限界があります。

そもそもエンコーディングの推測ということ自体が、実はかなり無理なシロモノで、過去において NKF.guess の主な仕事だったと思われる EUC-JP と シフトJIS の判別についても、「あまり長くない EUC-JP 文字列と、半角カナばかりのシフトJIS文字列」というような(少々人為的ですが)判別が絶対に不可能な例があるほどです。

また現代ではとりあえず、最初に「UTF-8のように見えるならばUTF-8だとみなす」という判定を入れたい、などと思うことがありますが、そういう調整などを入れることも、オートマジカルな「NKF.guess 任せ」では難しいでしょう。

現代的なエンコーディング推測

((この1文だけ余談)専ら通用しているエンコーディングという表現を使っていますが、EUC-JP などは正確には EUCエンコーディングで、EUC-JP などを指す場合は厳密には「コードセット」となります)

現代的なやりかたは、ファイルの内容を最初から最後まで全部確認することでしょう。

全て読み込むとスラッシングが起きる巨大ファイルであるとかいう場合には、何か別の方法を使う必要がありますが、そうでなければ、ファイルをまず全部 String として読み込みます。

その際、エンコーディングを指定しないことが明示的なメソッドである、IO.binread メソッドを使うのが良いでしょう。

そして、その String オブジェクト s のオクテット列が、目的のエンコーディング e による文字列として valid かどうかは次のように、String#valid_encoding? メソッドでチェックできます。文字列に「付いているエンコーディングの情報」だけを強制的に設定する force_encoding メソッドを使うのがミソです。

s = IO.binread 'file.txt'
e = Encoding::UTF_8  # 例として UTF-8 についてチェック

s.force_encoding e
if s.valid_encoding? then
  print "UTF-8\n"
else
  print "not UTF-8\n"
end

後はこれを、Stringオブジェクトとエンコーディングを渡すと判定してくれるメソッドとして分離して(force_encodingによる副作用の存在に注意)、上位層で例えば、

  1. まず UTF-8 のようであれば、UTF-8 だとみなす
  2. そうでなければ、シフトJISEUC-JP を試す
  3. シフトJISEUC-JP のどちらでもなければ、とにかく UTF-8 だとする
  4. シフトJISEUC-JP の、どちらか片方で valid なら、それを選ぶ
  5. シフトJISEUC-JP の、両方で valid なら、特製の判定関数を使う

とでもいったような手続きとして実装しておけば、仮に将来、なにか誤判定で困るようなパターンがあっても、柔軟に対応できるでしょう。

「James Clark 記法」に関するメモ

備考1

備考2

これは「ときに要素間ホワイトスペースの量を最小化するために使われる一般的でないスタイル」などと呼ばれるような手法のことを指している。

<!DOCTYPE Html>
<Html
 ><Head
   ><Title
     >2022年夏季卒業式</Title
   ></Head
 ><Body
   ><H1
     >卒業</H1
   ><Section
     ><……
 …………
 …></Section
   ><Section
     ><H2
       >卒業生</H2
     ><Ul
       ><……
    ………
 ……></Ul
   ></Section
 ></Body
></Html>
http://www.html5.jp/tag/elements/section.html

備考3

なお、「James Clark notation」というフレーズは、James Clark によるXML名前空間に関するメモ「XML Namespaces」( http://www.jclark.com/xml/xmlns.htm )などで使われている、「プレフィックス : 」の部分を実際の URI に展開して波括弧で囲んだ、

{http://relaxng.org/ns/structure/1.0}element

というような記法のことを指して使われていることが(英語圏では)ある( http://dvreeze.github.io/yaidom-and-namespaces.html )。

X Window System の Meta キーを設定した話

Macのキーボードにはコマンド(Cmd)キーがあり、シフト・コントロール・オルト(Alt)と同様の修飾(モディファイヤ)キーとして使えます。

Linux等ではメタ(Meta)キーが相当するキーとして扱える場合がありますが(たとえば、MozillaJavaScript のドキュメントにある表にそうある https://developer.mozilla.org/ja/docs/Web/API/KeyboardEvent/getModifierState#Modifier_keys_on_Gecko )手元の環境(FreeBSD)の X Window System で単にキーアサインを設定しただけではうまく行かなかったので、その顛末です。

まず X Window System の基本事項を確認します(X Keyboard Extension (XKB) のことは、とりあえず忘れます)。

X Window System のキーボードまわりには、

  • キーコード
  • キーシム
  • 修飾

の3要素があります。それぞれ、

  • キーコードはキーボードの各ボタンに対応した番号で、基本的には直接に機能には対応しない
  • キーコードからキーシムにマップされることで、何らかの機能と結び付けられる
  • シフト状態などの修飾はキーシムからさらにマップされる

といったようになっていて、xev というツールでXのイベントとして確かめてみると、

state 0x4, keycode 37 (keysym 0xffe3, Control_L)

(抜粋)といったように表示されますが、ここで state とあるのが、修飾キーの状態を示すマスクです(なお、state はその修飾キー自身の押下時のイベントでは設定されない)。

失敗篇

手元のキーボード(ユーシーテクノロジ(株)製造・パーソナルメディア(株)販売の「μTRONキーボード」)で手頃なキー(「Menu」キー)のキーコードが 117 だったので、

keycode 117 = Meta_L NoSymbol Meta_L

のように設定してみたのですが、なぜか修飾としてはAltキーと同じ扱いとなってしまい、うまくいきませんでした。

状況確認

(実際にはここであれこれ悩んでいたわけですが)xmodmap コマンドで修飾のマッピングを確認してみると、

$ xmodmap
xmodmap:  up to 4 keys per modifier, (keycodes in parentheses):

shift       Shift_L (0x32),  Shift_R (0x3e)
lock        Caps_Lock (0x42)
control     Control_L (0x25),  Control_R (0x6d)
mod1        Alt_L (0x40),  Alt_R (0x71),  Meta_L (0x9c)
mod2        Num_Lock (0x4d)
mod3
mod4        Super_L (0x73),  Super_R (0x74),  Super_L (0x7f),  Hyper_L (0x80)
mod5        Mode_switch (0x8),  ISO_Level3_Shift (0x7c)

と、なんやら妙なことになっています。これでは同一視されてしまうに決まっています(Meta_Lのコードが0x9cになっているのは、前述の設定より前の状態)。

余談ですが、今までキーコードとキーシムの設定にしか xmodmap コマンドを使ったことがなかったので、なんか変な名前だなと思っていたのですが、引数無しのデフォルトで表示される本来はこちらがこのコマンドの主目的で、そこから派生したとか、多分そんな理由なのですね。

ついでに関係しそうなキーコードとキーシムのほうも調べてみると、

$ xmodmap -pke | grep -e Alt -e Meta
keycode  64 = Alt_L Meta_L Alt_L Meta_L
keycode 113 = Alt_R Meta_R Alt_R Meta_R
keycode 125 = NoSymbol Alt_L NoSymbol Alt_L
keycode 156 = NoSymbol Meta_L NoSymbol Meta_L

となっていました(これも、起動後の初期状態のもの)。

解決篇

状況がわかってしまえば簡単で、今度からは一発で設定できるように次のようなファイルを作っておきます。

$ cat my_modmap.txt
clear mod1
clear mod3
keycode 125 =
keycode 156 =
keycode  64 = Alt_L NoSymbol Alt_L
keycode 113 = Alt_R NoSymbol Alt_R
keycode 117 = Meta_L NoSymbol Meta_L
add mod1 = Alt_L Alt_R
add mod3 = Meta_L

モディファイヤを設定する時に、キーマップの状態からの影響があるので、「モディファイヤをクリア、キーマップを設定、モディファイヤの再設定」という順序になるようにします。mod1がAltキーのモディファイヤ、mod3がMetaキーのモディファイヤです。

謎のマッピングがあったキーコード125と156はマッピングを消しておきます。Altキーがシフト状態ではMeta扱いになるという設定も消して、単純に Alt_L と Alt_R にマッピングします(ここでは示しませんでしたが、左右のShiftとControlは元から同様になっています)。Meta_L も同様に設定します。

最後にモディファイヤの設定をします。Meta_R も、設定だけでもしておきたい気もしますが、ここではそのままLだけを設定しました。

設定した後で、状態を確認すると、以下のようになります。

$ xmodmap my_modmap.txt
$ xmodmap
xmodmap:  up to 4 keys per modifier, (keycodes in parentheses):

shift       Shift_L (0x32),  Shift_R (0x3e)
lock        Caps_Lock (0x42)
control     Control_L (0x25),  Control_R (0x6d)
mod1        Alt_L (0x40),  Alt_R (0x71)
mod2        Num_Lock (0x4d)
mod3        Meta_L (0x75)
mod4        Super_L (0x73),  Super_R (0x74),  Super_L (0x7f),  Hyper_L (0x80)
mod5        Mode_switch (0x8),  ISO_Level3_Shift (0x7c)
$ xmodmap -pke | grep -e Alt -e Meta
keycode  64 = Alt_L NoSymbol Alt_L
keycode 113 = Alt_R NoSymbol Alt_R
keycode 117 = Meta_L NoSymbol Meta_L

Antec P182マシン、大改修・完結篇?

というわけで、その後以下のように手を加えました。

  • 大騒ぎの原因の一つだったグラフィックカードですが、結局ロープロファイルでショートサイズのカードに変更
  • PCIスロットの空きスロットの蓋を、空気穴が無いタイプに総取替
  • 上面排気ファンは、中段前面で補助吸気ファンとしての使用に変更。排気穴は物を置いて塞いでいる
  • 排気ファンは煙突(?)で上段前面側に排気(5インチベイを全部潰して14センチファンを最低回転数で利用)
  • BIOSの設定で最低回転数(PWMのデューティ比20%)まで落とす方法がわからなかったため、PWM制御信号を発生するArduino工作品を追加
  • CPUクーラーをScytheの兜3に変更。手頃な大きめのトップフロータイプなら何でもよかったのですが、日本ブランドのScytheで、ということで
  • ファンレスグラフィックカードヒートシンクが「触るとちょっと熱いかな」ぐらいになるので、PCIスロットに付けるタイプのファンステイ(アイネックスブランド。作りは長尾製作所っぽい気もするが、長尾からは販売されていない? タイプのもの)に無理矢理斜めに付けた、9センチファンで補助冷却(こちらも回転数を下げて利用)

CPUクーラーの空気流の余りで周辺も冷却したい所なので、兜3標準の12センチファンから14センチファンへの装換を実験していたが、ワイヤークリップを自作する必要があり、さらに14センチファン(Scythe製品ではないものを使用)の個体の問題か振動がひどく異音を発生させるために断念。

だいたいこんな所でしょうか。

Antec P182マシン、大改修

前に書いた通り、電源とマザーボードを新調したわけですが、この際ということで大幅な改修を行うことにしました(画像は改修後)。

  • 大型のグラフィックボードとの干渉で難儀していたHDDを下段に移動、中段ケージを撤去
  • 同様に、張り出していて、パラレルATAケーブルが邪魔でもあった光学ディスクドライブも撤去
  • 元々は強制排気・自然吸気の、強い負圧タイプが前提のケースだが、下段前面にファンを追加し強力に吸気を行うことで、HDDの強制冷却と同時に、ケース全体を正圧に近づける
  • 下段と中・上段のエアフローを分離する当て板は撤去し、下段吸気は積極的に上にも流す
  • (下段の中間に元々あったファンは以前に撤去済み)

という方針で、完全な再組立を行いました。サーバ的な管理された環境にあるマシンであれば負圧のケースも良いのかもしれませんが、ホコリを避けられない環境ということもあり、フロントパネル内側など、吸気のせいで、かなりえらいことになっていました(正圧にこだわるなら SilverStone あたりに買い替えたほうが素直かも?)。

まずこのケースで難儀するのが下段前面ファンで、P183など後継機と違い、それっぽい空間があるものの12cm角ファンの設置はかなり無理をともないます(こちhttp://day8ge.blog15.fc2.com/blog-entry-503.html の報告にありますように、不可能ではありませんが)。

ひとまわり下のサイズの9cm角(25mm厚)のファンで検討してみたのですが、実際に収めてみると、下段ケージのハードディスクとの間にほとんど間隔が無く、ファンの性能や騒音にも悪影響がありそうで、諦めました。今回の私の選択とは逆に中段にHDDをまとめ、下段がガラ空きであるとか、マウンターを選べばSSD化で前面側に余裕があるとかであれば、(フィルタの爪との干渉の問題もありますが)ここにファンを置くのも悪くないでしょう。

私は思い切って、ケースの前にファンを取り付けてしまうことにしました(写真では側面が写っています)。当然、扉は閉められなくなるので取っ払ってしまい、防音はかなり諦めることになります。通風のため、ケースの金網状の部分は、ファン取付のための角の部分を除いて、ハンドニブラで切除してあります(四隅の角が、ほぼちょうど12cmファンの取付穴の位置にあります)。ホコリ対策は十全にする必要があるので、フィルタを挟み込むタイプの組立式のファンガードを付け、100円ショップにあった使い捨ての、換気口用の15cm角の不織布フィルタを入れています。

(以下、続き)

通風改善のため、電源のATX24線とCPU Power Connector8線は裏配線に回しました(後継のケースでは裏配線の使い勝手はかなり改良されていますが、P182では上下チャンバの分離が優先されていて、あまりたくさんは使えません)。その他、下段にあるので結構長さが余るSATAの電源線などは、中段にのたくらせることで下段および全体の通風の改善を図っています。

また、チップセットとCPU電源部の冷却のための、4cm角で10mm厚のファンがユーザ追加のオプションで取り付けられるマザーボードで、メーカーによる解説では吸気になっているのですが、やはりバックパネル付近がホコリを吸うので(バックパネルの吸入口からダクトを付ける手もありますが)排気の向きで付けました。すると全体のエアフローの設計として、ケース側であるP182の後部排気ファンを付けると排気を奪い合ってしまいますから、上部だけの排気としました。

チップセットヒートシンク付近の構造物の形状が吹付け前提で、逆にすると効果がだいぶ落ちるんじゃないかという気はしますが(そういえばこれまでどこにもマザーボードについて書いてないですが、Sabertooth 990FX です)、マニュアルにも書いてないギミックですので気休めだと思って気にしないことにします。

この4cmファンには、山洋のものでこのサイズでは回転数が最も高いの(6200RPM, 109P0412H901)を選びました。絶対的な騒音としては他の大径ファンのほうが大きいものの、高音の騒音が出るので取付方法を改造したいと思っている所です(その他にもこの日記を書き始めた後にあれこれ手を加えているのですが、それはまた別の記事とする予定)。