Kara-age

赤福プラスが生成するHTMLのフッタ部分のうち、クレジットの上部は長らく中身が空っぽの矩形になっている。かつてはふたばにamazonの広告が表示されていて、矩形はそのためのものだった。しかしamazonの広告が復活する見込みはなさそうなので、別の利用法を考えてみよう。

  • 自前の広告を貼る: ふたばはあくまで人様のサイトである。そんなことが許されるのか?
  • 何か機能を持った領域にする: 例えば閉じたスレの一覧みたいなのがあったら便利…かな?
  • 特に何の機能もない領域にする: なんか適当に画像貼っておく

どうもいいアイデアが思い浮かばない。最後のやつでいいかな。

特に何の機能もない領域

tagging an image


画像を保存する際のファイル名のテンプレートに[cci]$TEXT[/cci]というものを新設した。これはキタ━━━━━━(゚∀゚)━━━━━━ !!!!!を除く最初のコメントに展開される。

レス画像を貼れる板の場合、レス画像を貼ってありかつコメントはキタ━━━━━━(゚∀゚)━━━━━━ !!!!!であるレスの引用は、そのレス番号を用いるのだそうだ。というわけでレス番号をクリックした際はそういうふうに振る舞うようにした。

webm/mp4を選択した際、それらもプレビュー表示の対象になるようにした。

ところでファイル名にコメントが含まれることのメリットというのは検索性の多少の向上というものがあるのだと思うけど、完全ではない。上の画像で言うとラーメンならラーメン、つけ麺ならつけ麺、豚野郎と犬子ならそういった文言がファイル名に含まれていれば完璧だが、そうなっているのもあれば、なっていないのもある。

画像を渡すとそれをAIが解析して、適切なタグ群を返すwebサービスみたいなのがあればいいのに。

Attach an image from clipboard

ついで、クリップボードに画像が格納されている時、コメント欄で Ctrl+V を押すとそれを直接添付ファイルにする機能を実装してみよう。

基本的にはコメント欄の paste イベントで clipboardData.files を見てその中から画像っぽいものを取り出し、それを覚えておき、投稿時に upfile 要素の内容の代わりに使用する、だけ。

のだが、いくつかめんどくさい点がある。

  • 貼り付けた画像のプレビューを生成しなければならない。これ自体は普通に upfile 要素で選択された画像をプレビューする処理と同じで何か新しくコードを書くわけではない。ただし、基本的な流れは画像を示す file インスタンスの内容を img 要素に割り当て、読み込み完了後に canvas 要素に縮小描画するというもので、特に file→img を従来は data スキーム経由で行っていた。しかしこれはけっこう重い。そんなわけでそこを createObjectURL/revokeObjectURL で行うようにした。今までそうしなかったというのはつまり Presto Opera でも動かすためだ。でももう Presto Opera のことは忘れる
  • 貼り付けられた画像のサイズが、板に貼れる上限を超えている場合。クリップボードに格納されている画像は、おそらく確実にブラウザへは image/png 形式で渡ってくる。png なのでサイズ的には常に大きめだ。それが板の上限を超えていた場合は jpeg でエンコードし直す必要がある。

    その場合は file を canvas に描画した後 toDataURL(‘image/jpeg’) で jpeg エンコードされた data スキームの URL を得て、それを XHR で読み込んで arraybuffer として取得し、そこから blob を生成…となる。途中で生成した canvas はそのままプレビュー生成にも使う。この辺、なんか回りくどい。canvas に blob を返すエンコードメソッドを設けてくれれば、それを FileReader から好きに読み込めばいいので汎用的だと思うのだけど…。

    あと、真面目に作るなら、jpeg エンコードしてもなお上限を超えている場合を考慮して、段階的に jpeg のクオリティを下げていくようなループにするべきなのだが、そこまではしなかった。

    ちなみに板に貼れる上限というのは注意書きの中で例えば「2000KBまで」とか書いてあるのをパースして覚えておくのですが。2000KB というのは 2000*1024=2048000byte のことでいいんだろうか。それとも 2000*1000=2000000byte か? 試してみた所 2000000byte 超のファイルは受け入れられるようなので前者とみなすようにした

だいたいこんな感じ。

Saving an image via Akahukuplus

虹裏にはたまにいわゆる虹裏ブラウザを話題とするスレが立つのだが、赤福プラスが俎上に載ることはめったにない。そしてまれに載った際はたいてい、使いにくくて機能の少ないクソと烙印を押されるのがオチなのであった。不満の一つに画像の保存をローカルに対して行えないという点があるようなので、それに対応してみよう。

もちろん Chrome のエクステンションからローカルのファイルシステムを直接は操作できないのだが、拙作のLFO – ローカルファイル操作ライブラリを通すと不思議なことにファイルの読み書きは自由にできちゃうので、これを使う。すでに wasavi と kokoni が LFO を使用している。これに赤福プラスも加わることになる。

LFO 側で適宜設定を行ったあと、赤福プラスの設定でファイル保存名テンプレートを[cci]ピクチャ/ふたば/$SERVER/$BOARD/$YEAR-$MONTH/$DAY-$SERIAL.$EXT[/cci]などと設定し、使用するストレージを[cci]local[/cci]にする。で、保存リンクを押せばそのとおりの場所に保存される。これだけ。あとは画像を開いた際に自動的に保存、みたいなオプションもあればいいかな。

技術的には、任意のパスに保存するには画像本体の保存に先駆けていわゆる [cci]mkdir -p[/cci] 的な動作が必要になる。現バージョンの LFO はこの機能を持っていないのでまずそこから実装する必要があった。LFO というか、Chrome Apps の FileSystem API 自体にそんな素敵な機能はない。従って、パス中の個々のディレクトリごとに一つずつ掘っていくという繰り返し処理を書くことになる。これを FileSystem API を直接呼びながら実現するとコードがものすごく汚くなりそうだったので、Promise を使うようにした。ついでなので、他の LFO のコマンドもすべて Promise ベースで動作するように書き直した。

ちなみにこの機能は Chrome 専用だ(Chrome Apps が Firefox や Opera で動くならその限りではないが)。

Prevent from executing an inline script

以前にも書いた気がするが、赤福プラスはふたばが返す html をまるごと変換し、上書きする。従って、元の html に記述されている画像や、スクリプトや、インラインフレームの読み込みはまったく不要で、ブロックする必要がある。ブロックした上で、変換後の html から改めて読み込まなければならない。

そんなわけで、Chrome では WebRequest API を用いてそれを実現していたのだが、ただ一つ html に直接記述された script 要素、つまりインラインスクリプトの実行は見逃していた。まあこれを見逃しても src 属性付きのスクリプトの読み込みはブロックしているので、大抵のインラインスクリプトはなんちゃらが定義されていませんエラーになって実害はないのだが、気にはなる。

しかし、Chrome のエクステンションの API を眺めてみてもインラインスクリプトの実行をブロックする機能は見つからない。いやあることはある。例えば WebRequest でレスポンスヘッダに CSP を忍ばせて、インラインスクリプトを除外するとか、あるいは ContentSettings でふたば上のみの javascript の実行を禁止するとか。が、リファレンスを読んでみるといずれも html を読み込んでから DOMContentLoaded までの短い期間だけスクリプトの実行を抑制するという要件にはちょっと合ってない。

いろいろ調べてみたところ、content script で MutationObserver を用いて script 要素が DOM ツリーに追加された瞬間をキャッチし、type を text/javascript とか application/javascript から、スクリプトとして実行されないものに書き換えるとそういう動作を実現できるようだ。で、用が済んだら、DOMContentLoaded ハンドラで disconnect() すればいい。完璧だ。

ただし Firefox ではこの type 書き換え法が効かないので、その代わり script 要素の beforescriptexecute イベントをリスンして適宜 preventDefault() する。つまり実行タイミングを start_at にした content script から

var observer = new MutationObserver(ms => {
. function handleBeforeScriptExecute (e) {
. . e.target.removeEventListener(
. . . 'beforescriptexecute', handleBeforeScriptExecute, false);
. . e.preventDefault();
. };
. ms.forEach(m => {
. . m.addedNodes.forEach(node => {
. . . if (node.nodeType != 1 || node.nodeName != 'SCRIPT') return;
. . . node.type = 'text/plain';
. . . node.addEventListener(
. . . . 'beforescriptexecute', handleBeforeScriptExecute, false);
. . });
. });
});
observer.observe(document.documentElement, {
. childList: true,
. subtree: true
});

とこんな感じのコードを走らせる。

ちなみに Presto Opera だとこんな長いコードを書かなくても、

window.opera.addEventListener('BeforeScript', function(e){e.source=''}, false);

だけで実現できる。オーパーツすぎる…。

そういえば忘れていたけど、次のリリースから赤福プラスは Presto Opera をもうサポートしない。

Integration #2

そういうわけで、設定のうち例外リストとなっていたものを site overrides とした。こんな感じで記述する:

http://example.org/* * block
http://example.net/*.html * writeas=p

ディレクティブとして有効な行は、3つの要素からなる。左から順に URL パターン、CSS セレクタ、アクション。まず URL パターンと CSS セレクタで要素を特定する。例外リストの段階では CSS セレクタは省略可能で、省略した場合ユニバーサルセレクタを指定したことになっていたが、省略不可能になった。サイトすべての編集可能要素を対象にしたい場合はユニバーサルセレクタを明示的に記述する必要がある。

最後の要素がアクションで、これが [cci]block[/cci] であった場合は、そのパターンに合致する要素上では wasavi の起動が抑制される。それ以外であった場合、set コマンドの引数として wasavi 起動時に評価される。つまり、exrc の最後に付加される。

 * * *

これとは直接は関係ないのだけど、バックグラウンド側での設定の持ち方を変えた。従来は各ブラウザの差異を吸収するような抽象的なクラスを経由していたが、単に chrome の API を直接呼ぶようにした。

Integration

wasavi のオプションに、writeas というものがあり、こういう仕様になっている。つまり、contentEditable な要素に対して wasavi のバッファを書き戻す際、どのような DOM の構造にするかを指定する。たとえば [cci]p[/cci] なら、各行を [cci]p[/cci] 要素として書き戻す。

しかし、当然ながら contentEditable な要素の内部構造をどのように保持しているかはサイトごとにまちまちであり、ひとつに決められるものではない。そこで、writeas には 連想配列を表す json 文字列を代入することも許している。こんな感じで:

set writeas='{ \
"http://example.com/*": "div", \
"http://example.net/*": [ \
{ \
"selector": "#any-css-selector", \
"writeas": "textAndBreak" \
} \
] \
}'

連想配列のキーが URL パターンで、値が writeas という形式だ。または、さらに連想配列の配列を入れ子にして、CSS セレクタを指定することもできる。これにより、つまりサイトごとに writeas の定義を分けることができる。

ただ……見てのとおり exrc 中に改行のエスケープしまくりで書かないといけないので、とても面倒くさい。また [cci]set all[/cci] した時の見栄えもとてもよくない。もっと洗練された形式で保持する必要がある。

ところで、サイトごとに定義を振り分けているものは wasavi はもうひとつ持っている。サイト自身が提供するスクリプトとコンフリクトするか何かでうまく動かない場合に、wasavi の起動をしないという例外リストである。これは普通のプレインテキストで

http://example.com/
http://exmaple.org/ #some-id

のように行ごとに URL を書き連ねる。URL のあとに任意の個数の空白を挟んで CSS セレクタを記述することもできる。これと writeas の定義を統合できないだろうか。つまり例外リストではなく per-site overrides という扱いにしたい。

override := URL-pattern (CSS-selector)? action
action := allow-actions
"block"
allow-actions := allow-action ("," allow-action)*
allow-action := "writeas" "=" writeas-value
writeas-value: "div" | "p" | "textAndBreak" | "plaintext" | "html"

という感じだ。

Mapping more modes #2

そういうわけでいろいろ変更している。

  • 特定のキー入力を別の入力に置き換えるために、最新の wasavi は以下の 3 種のマップを保持している:
    1. normal マップ: normal モード時に参照される
    2. bound マップ: bound/bound_line モード時に参照される
    3. input マップ: insert/overwrite モード時に参照される

    これ以外のモードにおいてはリマップはできない。これは技術的にできないのではなく、単に現状ではそういうふうにしていないというだけだ。

  • マップに対するルールの操作は、ex コマンド [cci]map[/cci]、および [cci]map![/cci] で行う。既定では、前者は normal と bound マップの両方を、後者は input マップを対象にする。どのマップを対象にするかは、後述のアトリビュートで変更できる。
  • [cci]map[/cci] [cci]map![/cci] は、最小で 0、最大で 2 つの引数を取ることができる。さらに、引数の前にアトリビュートを置くことができる。
  • アトリビュートは、先頭が [cci][[/cci]、末尾が [cci]][/cci] であるカンマ区切りの文字列である。アトリビュートのコンポーネントとして有効なものは、上記のマップ名か、もしくは [cci]clear[/cci] [cci]final[/cci] [cci]noremap[/cci] のいずれかである。
  • アトリビュートにマップ名が含まれる場合、デフォルトマップの代わりにそのマップが選択される。
  • アトリビュートに [cci]clear[/cci] が含まれる場合、選択されたマップに定義されたルールをすべて削除する。引数は使用されない。
  • 引数が 0 個の場合、選択されたマップに定義されたルールをすべて表示する。
  • 引数が 1 個の場合、lhs に引数 1 が部分的にマッチするルールをすべて表示する。
  • 引数が 2 個の場合、引数 1 を lhs、引数 2 を rhs として、選択されたマップ全てに対してルールを登録する。この際、アトリビュートに
    [cci]final[/cci] または [cci]noremap[/cci] が含まれる場合は、再帰展開しないマップとして登録される。

 * * *

ところでここまで修正したのを Selenium でテストしようと思ったら、何やらおかしい。wasavi を起動させたあと、キー入力が行われない。つまり textarea に対する sendKeys() は動くのだが、wasavi 本体の iframe への sendKeys() が動作しないようだ。

以下の chromium の issue が関係しているかもしれない:
https://bugs.chromium.org/p/chromedriver/issues/detail?id=1777
https://bugs.chromium.org/p/chromedriver/issues/detail?id=1819

Mapping more modes

wasavi の issue として、bound/bound_line に normal ではない個別のマッピングを行いたい、というものがある。

現状では、normal/bound/bound_line は MapManager 内部の command マップに収斂、insert/overwrite は edit マップに収斂、それ以外のモードはマップ不可、という形になっている。したがって、要件に応えるには、単に bound/bound_line 用の個別のマップを設けるだけでいい。

ただ、インターフェースの問題がある。ex コマンド [cci]map[/cci] は command マップを操作する。一方 [cci]map![/cci] は edit マップを操作する。[cci]![/cci] の有無の二者択一のため、第3のマップが入り込む余地がない。

vim では、これを nmap/vmap/xmap/smap/omap/imap/lmap/cmap… とむやみに ex コマンドを増やすことで対処している。さらに再帰的なマップ展開をしない版である nnoremap/vnoremap/xnoremap/snoremap/onoremap/inoremap/lnoremap/cnoremap… という群もあり、総数としてはかなりのものになっている。

これ、果たしてわかりやすい仕様なんだろうか。だいたいなんなの onoremap とか inoremap って。初見じゃあ己マップと祈れマップとしか読めないよ。何をするコマンドなのかさっぱりわからない。

どうもこんな感じに ex コマンドをホイホイ新設するのは良くない設計に思える。加えて自己記述性の低いコマンド名はもっと良くない。

そんなわけで、wasavi では noremap かどうかを指定するのは map コマンドに与えるアトリビュートという形式にしてある。アトリビュートとはつまり
:map [noremap] gh ^
などと LHS の前に角括弧つきで指定する部分のことだ。ちなみに noremap というアトリビュート名もなんかいまいちなので、例えば final にしようかと考えている。

で、定義先のマップもアトリビュートで指定するようにしてはどうだろうか。例えば
:map [bound,final]
みたいな感じ。map コマンドを投入してかつアトリビュートでマップを指定しなかった場合に選択されるデフォルトのマップは normal と bound の和集合になる。一方、map! コマンドの場合は対象は edit マップ。

また、引数なしで map/map! コマンドを投入した場合は現在定義されているマップを表示するが、その場合には対象となるマップも表示したほうがいいかもしれない。

Screen Casting

wasavi の Readme の最初の画像2つは、素の textarea と、それに対して wasavi を起動した状態の対となるものにしてある。

ところで Twitter でたまに wasavi について tweet される際、それは最初の画像を添付してあったりする。前述の通り最初の画像は単に素の textarea の画像なので、wasavi についての説明になっていない。例えば:

これを解消するために、2つの画像をまとめ、スクリーンキャストによる gif アニメーションをこさえたい。素の textarea から wasavi を起動し、編集し、書き込んで終了するまでを録画すればいい。というわけで、gtk-recordmydesktop をインストールし、録画し、ogg theora 動画を生成した。それを適当なオンラインのコンバータで gif に落とした。

gif に落とす際のオプションとかはなかったのでディザが目立つが、まあこんなもんだろう!

* * *

適当なオンラインのコンバータに頼るのはやめ、ogv → gif 変換はローカルで ffmpeg により行うようにした。見苦しいディザは追放した、はず。