line number

行番号を表示したい!

ということでやってみた。

行番号を表示すると一気になんだか高機能そうなテキストエディタに見える不思議。

Fürstentum L10n #4

wasavi 本体の l10n は終わったが、まだオプションページとインストール時のメタ情報ファイルが残っている。

オプションページ
アーカイブ内の options.html をどうにかする。ところで勘違いしていたのだけど、Chrome で l10n 対象の文字列(__MSG_*__)が自動的に変換されるというのは、manifest.json と css ファイルだけだった。html ファイルは変換されない。しかしどのみち、Opera と Firefox では自前で変換しなければいけないので特に問題ではない。

オプションページにアタッチしたコンテントスクリプトで DOMContentLoaded イベントをリスンし、ページ内の __MSG_*__ を xpath で探し出し、変換後の文字列で置き換える。オプションページが ajax であれこれするページだともっとめんどくさいのだろうけど、いまのところはそうではないので、これだけ。

インストール時のメタ情報ファイル
このファイルの中で弄るのは具体的には、wasavi の説明文だ。これを l10n したい。”wasavi” という名前はまあ、別にどの言語でも同じでいいんじゃないかな?

  • Chrome
    Chrome では manifest.json がメタ情報に当たるが、前述の通り自動的な変換の対象なので、単に説明文を __MSG_wasavi_desc__ と置き換えるだけ。

    {
    "name": "wasavi",
    "version": "0.3.147",
    "description": "__MSG_wasavi_desc__",
    "default_locale": "en_US",
    "icons": {
    "16": "icon016.png",
    "48": "icon048.png",
    "128": "icon128.png"
    },
    :
    :
    }
  • Opera
    Opera では、config.xml 内に description 要素を置き、xml:lang を指定した上でそれぞれの言語の説明文を格納する。ソース中のメッセージカタログを走査して説明文を取り出し、xml 内に注入するスクリプトを書き、ant の build.xml でそれを呼び出すようにした。最終的にはこうなる:



    wasavi
    vi editor for any web page.
    :
    :
    vi editor for any web page.
    web ページ上の vi エディタ

  • Firefox
    Firefox では、install.rdf を弄る。ここに詳しい。

    ところで 1 つ気になることがある。Chrome と Opera では、メッセージカタログはそれぞれのエクステンションがネイティブで提供する l10n の仕組みに沿って格納しているが、Firefox ではそうではない。本来はエクステンションのアーカイブの /locale/[localeCode].json に置くのを、/resources/wasavi/data/xlocale/[localeCode]/message.json という Firefox のあずかり知らない場所と形式で格納している。

    これって AMO に登録する際にリジェクトの理由になったりするのかな? あるいは、リジェクトまでは行かなくても、エクステンションの一覧か何かで「このエクステンションはどの言語にもローカライズされてまへん」みたいな扱いになるかもしれない。

    それはちょっと悔しいので、wasavi の説明だけは Add-on SDK 標準の形式で各言語のメッセージカタログを置くようにしてみた。実行時に参照されることはないのだけど、一応どの言語へ l10n されているかは分かる。これもやはりそのためのスクリプトを書き、ビルド時に自動的に実行されるようにした。

    それから、Firefox でオプションページを開く手段として、アドオンバーにアイコンを追加するようにしていたのだけど、本当に単にオプションページを開くだけの機能しかなくてなんだかなーだった。しかし simple-prefsアドオンの管理ページにボタンを追加できるようなのでそうした。

    そうしたのだが、このボタンのラベルを l10n する手段がわからない。うーむこれはまあ分かったらいじることにしよう……。


ということでこんな感じに。

Fürstentum L10n #3

l10n のだいたいの形が決まったので、その通りに組んでみた。

おさらいする。3 種のブラウザで同様に動く l10n の仕組みを wasavi に組み込むのが目的。3 種のブラウザがそれぞれ l10n の仕組みを備えているが、wasavi 上では Chrome のそれを基本にし、他のブラウザでは Chrome の仕組みをエミュレートする形。Chrome の l10n の仕組みとは、エクステンションのアーカイブ内に _locales/[localeCode]/messages.json といったファイルを配置し、その json ファイルにメッセージ ID とそれに対応する各言語のテキストを格納し、変換は、wasavi 実行中に動的に行うというもの。

それに加えて、wasavi 独自の仕組みがある。

  • メッセージへのパラメータの埋め込み。平たく言うと sprintf() だ。これは Chrome が提供する i18n にも用意されているが、wasavi 側で行う。メッセージ中の {0} といった部分は、パラメータで置き換えられる(数値はパラメータのインデックス)。
  • メッセージ中、複数形になりうる名詞の変換。1 行削除したときは
    deleted 1 line.
    それ以上の行を削除したときは
    deleted 10 lines.
    のように自然な形のメッセージを生成する。ソース上は、メッセージは “deleted {0} {line:0}.” と記述する。{word:number} の形式だ。ここでも number はパラメータのインデックス。パラメータの値によって word を自然な形に変化させ、波かっことその中身全体を、生成した word で置き換える。

    これには、以下のものが必要になる:

    1. パラメータを引数にとって、変化パターンを返す関数
    2. 各変化パターンに対応する word の変化形

    これらの情報もまた、messages.json に格納する。まず 1. は、Language Plural Rules を参考に生成する。例えば英語の場合は

    Language Name Code Category Examples Rules
    English en one 1 one → n is 1;
    other → everything else
    other 0, 2-999;
    1.2, 2.07…

    ということである。変換関数は Rules を、変化パターンは Category を見ればいい。まず変換関数は、Rules に対応する javascript の式を書く。n が 1 の場合は ‘one’、それ以外は ‘other’(実際は、空文字で代用)を返すようにする。

    {
    "_plural_rule@function": {
    "message": "n==1?'one':''"
    }
    }

    そして、全メッセージ中で使用している複数形になりうる単語について、one パターンと other パターンのそれぞれを定義する。

    {
    "_plural_substitution": {
    "message": "substitutions"
    },
    "_plural_substitution@one": {
    "message": "substitution"
    },
    "_plural_line": {
    "message": "lines"
    },
    "_plural_line@one": {
    "message": "line"
    },
    "_plural_character": {
    "message": "characters"
    },
    "_plural_character@one": {
    "message": "character"
    },
    "_plural_operation": {
    "message": "operations"
    },
    "_plural_operation@one": {
    "message": "operation"
    }
    }

    メッセージ ID は “_plural_” + word の基本形 + @ + 変化パターンとなる。この後ろに、通常のメッセージ群についてメッセージ ID と変換結果を全て記述する。

    {
    "invalid_boolean_value": {
    "message": "Invalid boolean value",
    "description": "Invalid boolean value"
    },
    :
    :
    :
    "top_of_history": {
    "message": "Top of history.",
    "description": "Top of history."
    }
    }

    messages.json 側でやることは以上。

で、Chrome ではメッセージ ID を変換するには chrome.i18n.getMessage() を呼ぶだけでよいのだが、他のブラウザではそうは行かない。

  • Opera
    フォルダベースの l10n 機能を利用する。background の起動時に XHR でエクステンションアーカイブ内の /messages.json を読み込む。実際は、メッセージカタログは locales/[localeCode]/messages.json に配置してあり、自動的にロケールに応じたファイルが参照される。wasavi 起動時にメッセージカタログの内容を wasavi.js へ送信する。wasavi.js 側でメッセージが必要になった際は、自分でカタログの中から探し出す。
  • Firefox
    メッセージカタログは data/xlocale/[localeCode]/messages.json へ配置する。現在のロケールを得るために api-utils/l10n/locale モジュールの getPreferedLocales() を使う。また、有効なロケール群から現在のロケールに最もふさわしいロケールを抜き出すためにも、上記のモジュールが提供する findClosestLocale() メソッドを使う。これ以外に、標準的な l10n の機能は使わない。
    その他の仕組みは、Opera と共通。

というわけで、とりあえず日本語ロケールのメッセージを作ってみた。

Fürstentum L10n, again

L10n, つまり Localization をやる段階になった。

再度各ブラウザに用意されている l10n(あるいは i18n)のリンクを引いておく。

l10n といってもやることはいろいろあると思うけど、wasavi においては、wasavi からのメッセージをユーザが求める言語に変換した上で出力することとしよう。ソース上では、l10n の対象になりそうなメッセージの文字列リテラルは、すべて関数 _ の呼び出しにラップしてある。したがって、変換処理は _ の内部に閉じ込めればいい。

Continue reading

the last

ex コマンド最後のテスト、@ コマンド。

@ コマンドは、指定のレジスタに格納されている文字列を ex コマンドとみなし、実行する。vi コマンドにも @ があり、vi コマンド版 @ は ex コマンドを含むことができるのでつまり上位互換ということになっている。ex コマンド版の @ はレジスタの内容をあくまで ex コマンドとして実行する。

[addresses]@register

レジスタが省略されたか、文字 @ が指定された場合は、最後に実行されたレジスタ名が用いられる。レジスタの実行がまだ 1 度も行われていない場合は、エラーになる。

addresses で指定された範囲の各行について、まず現在行( . )にセットされ、そしてその時点で @ コマンドが実行されたかのように、名前付きレジスタの内容が ex コマンドとして実行される。ex コマンドパーサは、行指向で保存されたレジスタの各行と、文字指向で保存されたレジスタの最終行以外の行は、末尾に改行がついたものとして扱う。

処理の間にエラーが発生した場合、または addresses で指定された行がそれを現在行としてセットする段階で存在しなかった場合、または addresses として複数行が指定されかつバッファの内容が(たとえば ex コマンド edit によって)置き換えられた場合、エラーとなり、エラーメッセージが表示され、このコマンドの実行は打ち切られる。

 * * *

最後の段落以外は、特に問題はない。最後の段落は実はよくわからない。たとえば現在行の次の行を削除するような ex コマンド(+1d とか)を、複数行を対象に、@ コマンドで実行したとする。すると、仕様で言っているような addresses には含まれているが、レジスタの内容を実行する段階では存在しない行というのが確かに生まれる。仕様では、そういう行を見つけたら即エラーにすると言っている。それはどうなのか? その動作は global コマンドとも異なっている(global コマンドはマッチした行を覚えておくフェーズと、それらの各行に対してコマンドをは実行するフェーズで構成される。第 2 フェーズのループ内では、第 1 フェーズにおいて覚えておいた行が存在しなくてもエラーにせず、次の行へスキップする)。

また、レジスタに格納されている ex コマンドが明示的に addresses を含んでいる場合、@ コマンドに対する addresses とどう折り合いをつけるかは仕様では示されていない。置き換えるのか、マージするのか、レジスタのそれを優先するのか?

vim はこの点について、@ コマンドに付随するアドレスは addresses ではなく実質的に address であるという仕様にしているようだ。これはちょっと特殊で、コマンド自体は複数行の範囲を受け付けるが、実行はその先頭行のみに対して行われる。先頭行を現在行に設定しレジスタの内容を実行する、それで終わり。ヘルプを読む限りでは将来的には複数行に対応するかもねということになっているが、posix による定義が曖昧なのが解決しない限り対応は難しそうだ。

ということで、wasavi でもそれに従う。

under the control

コードポイント U+0000 から U+001F はいわゆる制御文字とかコントロールコードと呼ばれる文字を表す。

この中で、今日日プレーンテキスト内によく使われるのは CR、LF、TAB、FF、VT くらいで、その他の文字はほとんど使われることはないのだけど、それでもなお存在はしている。

vi では、入力モード中や ex コマンド入力中に特別なシーケンスを用いて制御文字を入力することができる。まず ^V を押し、次に入力したい文字を押す(U+0000 から U+001F までは ^@ から ^_ に対応している)。

textarea に書き込むテキストにもまた、それらの制御文字が必要だとは思えないが、wasavi でも ^V によるエスケープを用いた入力に対応している。ただし、vim のように ^V + 8 進数によるコードポイント入力には未対応。

さて入力できたとして、それをどう表示するかが問題になる。本来は表示上も ^K とかいう形式にしたいのだが、wasavi では 1 行の表示はブラウザ任せなので、なかなかそこまで制御できない。制御文字に対応する部分を
^K
のような形にすればいいのだが、まだそこまでやっていない(編集するテキストが span によって分割されていることを前提にするのは、たぶんシンタックスハイライティングの実装とセットになるので)。

そこで、Unicode には Control Pictures というものがあるので、とりあえず wasavi が管理するバッファ内の制御文字は U+0000 -> U+2400 みたいな変換を施して保持している。もちろんバッファ内のテキストを外部に出力する際は本来の制御文字に逆変換する。

ただ、これだと Control Pictures の各文字が等幅じゃなかったり、ブラウザによって見た目が違ったり、下手すると表示されなかったり、制御文字ではなく U+2400 ~ U+241F そのものを扱えなかったりするわけなので、まあ本当に「とりあえず」の仕様だ。

ところで「等幅」の読みって「とうふく」だと長年思っていたら、なんかググると「とうはば」らしいですよ。大ショック。

quit, wq

ex コマンド quit は vi を終了させる。
q[uit][!]

コマンド名に文字 ! が後続していない場合:

  • 最後に完全な保存が行われてからバッファが変更されていれば、エラーにする
  • vi の起動時に与えた引数リストのうち、現在参照しているファイル以降の編集待ちファイルが待機しており、かつ、最後に実行したコマンドが quit、wq、xit、ZZ のいずれでもない場合は、エラーにする

コマンド名に文字 ! が後続している場合は、編集セッションを終了する。

引数リストというのは、
$ vi foo.txt bar.txt baz.txt
などというコマンドラインで vi を起動した際に与えたファイル名の配列 [foo.txt, bar.txt, baz.txt] のことだ。さて、foo.txt の編集中に :q する。すると、(vim の場合)2 more files to edit などというエラーになる。残りの bar.txt と baz.txt どないすんねんと怒っているわけだ。ただ、:q してさらに :q した場合などは、なんやそないに終了したいんなら言うとおりにしたるわ、と妙にユーザーフレンドリーな振りまいをし、終了する。

なお wasavi は引数リストをサポートしていないので、このルールは関係ないし関西弁でしゃべったりもしない。

 * * *

というわけで write と quit コマンドがあるのだが、実際にファイルを編集、特に数行編集して即 vi を抜けるなんていう場合、2 回 ex コマンドを投入したり、あるいは :w|q と打つのが面倒だということなのか、wq というコマンドが用意されている。

wq は write をまず実行し、成功したら quit を実行するという処理をひとまとめにして行う。wq のシンタックスは write と同じである。

See the write

ex コマンド w、q、wq、x。

これらのコマンドは、編集中のバッファの内容を書き出したり、vi 自体を終了したり、その 2 つのの動作をひとまとめに行ったりする。バッファの内容を書き出すことと vi を終了することには、本質的には強い関連性はないのだけど、実践的なコマンドの使用頻度を鑑みたのか、ex コマンドの体系では結び付けられている。

基本となるのは w(write) コマンドと q(quit) コマンド。

write
[addresses]w[rite][!][>>][file]
w コマンドはバッファの内容をファイルに書き出す。

通常、addresses を明示せずに ex コマンドを実行した場合、対象となる行は単にカレント行であるが、write コマンドは global や v コマンドとともに規定値がバッファ全行になっている。

ファイルへの書き込み
w コマンドのコマンド名に空白文字が後続していない、または file が文字 ! で始まっていない場合は、書き込みはファイルに対して行われる。

  1. 引数 >> が指定されており、かつ file がすでに存在している場合、バッファの内容はそのファイルの内容を置き換えるのではなく、追記される。>> が指定されているにもかかわらず file が存在していない場合、>> が指定されなかったものとして振る舞うか、書き込みが失敗するかは未定義である。
  2. readonly オプションがセットされている場合、書き込みは失敗する。
  3. file が指定されているが、それがバッファに結び付けられたファイル名ではなく、かつ file が存在する場合、書き込みは失敗する。
  4. file が指定されなければ、バッファに結び付けられたファイル名が用いられる。バッファにファイル名が結び付けられていなければ、コマンドは失敗する。
  5. バッファに結び付けられたファイル名が用いられ、かつそのファイル名は file または read コマンドによって変更されたものであり、かつそのファイルが存在する場合、書き込みは失敗する。一旦書き込みが成功した後は、後続する書き込みはこの理由で失敗してはならない(再びファイル名が変更されない限り)。
  6. バッファ全体を書き出せず、かつ書き込み先ファイルが存在する場合、書き込みは失敗したとみなす。

コマンド名に ! が続いている場合、ルール 1.、2.、3.、5. において書き込みは強制的に行われる。

writeany オプションがセットされている場合、ルール 2.、3.、5. において書き込みは強制的に行われる。

この他の実装依存の条件判定により書き込みが失敗することもある。

バッファが空である場合、空の内容で書き込みが行われる。

書き込まれた行数とバイト数の情報がメッセージとして表示される必要がある。

プログラムへの書き込み

コマンド名の後に 1 文字以上の空白が続き、かつ file の先頭が文字 ! である場合、文字 ! の後の残りの文字列は、文字 %、#、! について展開が行われる(展開の詳細は省略)。

ex/vi は shell オプションで指定されるプログラムを起動し、2 つの引数を渡す。1 つめは -c である。2 つめは write コマンドへの引数(展開済み)が単一の引数として渡される。バッファ内の指定された行は起動したプログラムの標準入力へ書き込まれる。プログラムからの標準エラーや標準出力があれば、print コマンドの出力のように表示される。出力の最後の文字が改行でなければ、改行が出力される。

write コマンドに続く文字 ! はバックスラッシュによって効果を打ち消すことができる。

Continue reading

finding a max z-index

wasavi の新しいビルドはだいたい週末にここにアップし、さらに某掲示板でアナウンスしているわけなのだが、その中に

クロームでふたクロ使ってると
フローティングのテキストエリアの下にviの編集ウインドウ開いちゃうね
調節する方法あるのかな
これわさびで入れてみたよ

というリプライがあった。ふたクロというのはその某掲示板向けの Chrome 用拡張だ。

さてこれは要素の上下関係の問題だ。具体的には、2 つの要素のそれぞれの z-index プロパティの値の関係だ。確認してみると、ふたクロが生成する textarea のその親となる form の z-index は 2000000013 とかそんな感じだった。20 億飛んで 13。一方で、wasavi が生成する iframe には 16777215 すなわち 0x00ffffff を入れている。したがって wasavi のほうが隠れるというわけだ。

直接的には、20 億飛んで 13 を超える z-index 値、たとえば 0x7fffffff を割り当てるだけの話だが、そんなんでいいのか? という気も。まずこの値は経験則的な最大値でしかない。32bit バイナリでブラウザを動かして、z-index がとりうる値は負の値を含む数値で……なんてところだとまあ最大値はたぶんこれだろう程度のものだ。しかし CSS2.1 の仕様では z-index の最大値には特に記述がない。すると、もしかしたら 64bit バイナリなブラウザではさらに大きな z-index 値を指定できる可能性もなくはないということになる。そうすると 0x7fffffff を指定したところで意味がない。

実はある時期までは、wasavi の起動時に全要素を舐めて、その時点での最大の z-index 値を算出し、それを超える値を wasavi に与えていた。これならどの環境でも問題ない。手法としては、やはりこれが正道だ。しかし、全要素を舐めるわけなので当たり前なのだが、算出処理がとても重いのだ。100ms オーダーで時間を要する。

次善の策として、全要素ではなく、対象となる textarea 要素をその親へ遡った部分ツリーだけを算出の対象にするという方法もある。これならまあせいぜい数 10 個の要素を舐めるだけの話だ。ただこれだと必ず最大の z-index を得られるわけではないんですけどね……まあしかたないかな。

そういうわけでそう修正。

!?