HTML のテキスト入力欄の入力値の一部を置換する2021年12月23日 23時48分

この記事は HTML アドベントカレンダーの 23 日目の分、兼 JavaScript アドベントカレンダーの 23 日目の分です。


HTML のテキスト入力欄 (<input type="text"> 要素や textarea 要素など) で選択範囲の文字列を置換したいとき、一昔前は JavaScript で以下のように書く必要がありました。

// 引数 field には HTMLInputElement オブジェクトまたは HTMLTextAreaElement オブジェクトを、
// 引数 newText には文字列を、それぞれ受け取る。
function replaceSelectionText(field, newText) {
  var start = field.selectionStart;
  var end = field.selectionEnd;
  var text = field.value;
  field.value = text.substring(0, start) + newText + text.substring(end);
  field.setSelectionRange(start, start + newText.length);
}

(さらに大昔は IE 向けに TextRange オブジェクトを使う必要がありました。)

今は HTML 標準で setRangeText メソッドが定義されているため、以下のように書けます。

function replaceSelectionText(field, newText) {
  field.setRangeText(newText);
}

setRangeText メソッドには置換する範囲を指定することもできます。テキスト入力欄 field の入力値が abcde のとき、以下のコードを実行すると入力値が axde になります。

field.setRangeText('x', 1, 3);

このとき、元の入力値において改行は \n に正規化され、1 文字として数えられます。また、「1 文字」というのが Unicode の符号位置ではなく UTF-16 の符号単位を表すことに注意が必要です。

置換する範囲を指定して setRangeText メソッドを呼び出したとき、選択範囲はメソッド呼び出しの前後であまり変化しないように調整されます。この挙動は、第 4 引数に selectstartendpreserve のいずれかの値を指定することで変更できます (省略時は preserve)。以下の例ではメソッド呼び出し後に部分文字列 x が選択されます。

field.setRangeText('x', 1, 3, 'select');

Web Platform Tests での HTML 標準のテストに触れる2021年12月22日 23時50分

この記事は HTML アドベントカレンダーの 22 日目の分、兼 JavaScript アドベントカレンダーの 22 日目の分です。


Web Platform Tests をご存じでしょうか。Web ブラウザ間の相互運用性を高めるための、様々な Web 標準技術に関するテストスイートです。主要 Web ブラウザは Web Platform Tests を開発プロセスに取り込み、互換性の向上やリグレッションの防止を図っています。

Web Platform Tests はあなたが今閲覧に使っているブラウザで実行できます。試しに HTML 標準に関するテストを実行してみましょう。https://wpt.live/ からディレクトリをたどって https://wpt.live/html/syntax/parsing/ に行くと、数多くのテストファイルが並んでいます。

ファイルリストの先頭にある DOMContentLoaded-defer.html を開いてみてください。あら、テストが失敗してしまいました!

DOMContentLoaded-defer.html を開くと 1 Fail と表示されます。

気を取り直して次の Document.getElementsByTagName-foreign-01.html を開いてみると……今度は成功しています! JavaScript から document.getElementsByTagName メソッドを呼び出して SVG の要素を取得できることを確かめられました。

Document.getElementsByTagName-foreign-01.html を開くと 37 Pass と表示されます。

ブラウザによっては失敗したテストをバグトラッキングシステムで管理しています。先ほど失敗した DOMContentLoaded-defer というファイル名を Mozilla Bugzilla (Firefox のバグ管理にも使われています) で検索してみると、1242128 - Frequent Win8 TEST-UNEXPECTED-PASS | /html/syntax/parsing/DOMContentLoaded-defer.html | The end: DOMContentLoaded and defer scripts - expected FAIL が出てきました。

議論の場はそこから DOMContentLoaded-defer.html is likely wrong · Issue #4267 · web-platform-tests/wpt に移っています。どうもこれはテストのほうがおかしいのではないかという話が出ていますね。

Web Platfrom Tests は絶対的なものではなく、日々追加・変更されています。開発は GitHub で開かれており、開発への貢献は各 Web ブラウザの品質向上につながります。筆者もかつて縦書きの文書におけるスクロール位置に関するテストを追加したところ、それに関する Chrome の挙動が修正されました

あなたも Web Platform Tests に参加して Web プラットフォームを進化させていきませんか!

HTML のフォームでアップロードしてほしいファイル種別を指定する2021年12月21日 23時14分

この記事は HTML アドベントカレンダーの 21 日目の分です。


HTML のフォームでファイルを送信するには、ファイルアップロードコントロール <input type="file"> を使います。このとき、accept 属性に MIME タイプまたは拡張子を指定することで、アップロードしてほしいファイルの種別を指定できます。PNG 画像または JPEG 画像をアップロードしてほしければ、以下のようにカンマ区切りで記述します。

<input type="file" accept="image/png, image/jpeg, .png, .jpg, .jpeg">

特殊な値として以下の三つが定義されています。これらの値も他の値と組み合わせて指定できます。

image/*
何らかの画像ファイルをアップロードしてほしいときに指定する。
video/*
何らかの動画ファイルをアップロードしてほしいときに指定する。
audio/*
何らかの音声ファイルをアップロードしてほしいときに指定する。

個別のファイル種別を指定するときは、MIME タイプと拡張子を両方組み合わせて指定することが勧められています。MIME タイプまたは拡張子、どちらかのみでファイル種別を管理しているシステムへの配慮からでしょうか。

MIME タイプにパラメータは指定できません。UTF-8 で符号化された HTML 文書のみアップロードしてほしいからといって、accept="text/html; charset=UTF-8" とは書けないのです。

accept 属性で指定できるのはあくまでもファイル名などに基づく表面的なファイル種別であり、Web ブラウザがファイルフォーマットをきちんと検証してくれるわけではありません。サーバー側でのファイルフォーマット検証を忘れないようにしましょう。

<input type="file"> にはほかにも、複数ファイル選択を可能にする multiple 属性や、デバイスのメディアキャプチャ機能を直接利用可能にする capture 属性を指定できます。capture 属性に関しては「captureでカメラ起動(知名度が低いウェブ標準ひとりAdvent Calendar 2021 – 17日目) | Ginpen.com」で紹介されているのでそちらも参照してください。なお、capture 属性はまだ HTML 標準に取り込まれていませんが、HTML Media Capture として W3C 勧告になっています。

HTML の img 要素と image タグ2021年12月20日 23時40分

この記事は HTML アドベントカレンダーの 20 日目の分、兼 JavaScript アドベントカレンダーの 20 日目の分です。


HTML のタグといえば、要素の開始位置、終了位置、そして属性を指定する記述のことですね。開始タグは小なり記号 (<) の後に要素名が続きます。しかしながら、タグに既述した名前とは別の名前の要素が生成される場合があります。

以下の JavaScript コードを実行すると、JavaScript コンソールには (IMAGE ではなく) IMG と出力されます。タグに記述された名前は image なのに、img 要素が生成されているのです。

const div = document.createElement('div');
div.innerHTML = '<image src="" alt="">';
console.log(div.firstChild.tagName); // => "IMG"

実はこれは HTML のパース処理における例外的な扱いで、開始タグの名前が image のとき、内部的にはパースエラーとしつつ名前を img に読み替えることになっています。Web 製作者の記述ミスを救済しようとする慈悲の心の現れですね。

あくまでも HTML のパース処理における例外なので、JavaScript で document.createElement メソッドを使って要素を生成するときには適用されません。以下の例では "[object HTMLUnknownElement]" という文字列が出力されるはずです。(しかし Firefox では "[object HTMLElement]" という文字列が出力されます。)

const image = document.createElement('image');
console.log(image.toString());

参考文献

Perl で関数内に関数を定義する2021年12月20日 21時08分

この記事は Perl アドベントカレンダーの 20 日目の分です。


Perl コードを書いていて、関数内で関数を定義したいと思ったことはありませんか? 普通に sub foo { ... } の中に sub bar { ... } を書けばよいのでは思われるかもしれませんが、それだと関数が局所的にならず外部に露出してしまいます。

package MyPackage {
    sub foo {
        sub bar { 42 }
        return bar();
    }
}
# foo 関数の外部からも bar 関数を呼び出せる。
say MyPackage::bar(); # => 42

無名関数のコードリファレンスを使うこともできますが、見た目がちょっと煩雑ですね。

package MyPackage {
    sub foo {
        my $bar = sub { 42 };
        return $bar->();
    }
}

そこで lexical subroutines の出番です。my sub foo { ... } または state sub foo { ... } と書くことで、局所的な関数を定義できます。Perl 5.18 で試験的に導入された機能で、Perl 5.26 から本採用となり警告なしで使えるようになりました。

package MyPackage {
    sub foo {
        my sub bar { 42 }
        return bar();
    }
}
# foo 関数の外部からは bar 関数を呼び出せない。
say MyPackage::bar(); # => Undefined subroutine &MyPackage::bar called

lexical subroutinessort 関数に渡すこともできます。

my sub compare { $b <=> $a }
my $array_1 = [ sort compare 1, 8, 2, 3, 5 ];
my $array_2 = [ sort compare 4, 7, 6, 0, 9 ];
say @$array_1; # => 85321
say @$array_2; # => 97640

注意点として、Perl の lint である Perl-Critic には、バージョン 1.140 時点で lexical subroutines に誤った警告を出してしまうという問題があります。

筆者はいずれの問題に対しても修正 pull request を提出しています。Perl-Critic の誤検知がなくなり、気兼ねなく lexical subroutines が使えるようになるとよいですね。