PerlのText::Markdown::Discountで囲い付きコードブロックを扱う2022年12月11日 01時31分

この記事はPerl Advent Calendar 2022の11日目の分です。


PerlのText::Markdown::Discountモジュールを使うと、MarkdownをHTMLに変換できます。

use feature qw(say);
use Encode qw(encode_utf8);
use Text::Markdown::Discount qw(markdown);

say encode_utf8 markdown(<<'MARKDOWN');
こんにちは、世界。

* 順序
* なし
* リスト
MARKDOWN
<p>こんにちは、世界。</p>

<ul>
<li>順序</li>
<li>なし</li>
<li>リスト</li>
</ul>

オリジナルのMarkdownにはありませんが、CommonMarkおよびその拡張であるGitHub Flavored Markdownなどには囲い付きコードブロック(fenced code block)が存在します。3つ以上のバッククォート(`)の並びまたはチルダ(~)の並びで囲んだ部分が、HTMLのpre要素とcode要素を使って出力されるというものです。

Text::Markdown::Discountモジュール(の内部で使われているDiscountというMarkdown処理系)でも囲い付きコードブロックに対応していますが、扱い方がバージョンによって異なります。

Text::Markdown::Discount 0.14以降

囲い付きコードブロックを有効にするには、markdown関数の第2引数にMKD_FENCEDCODEフラグ(0x02000000)を指定します。

my $html = markdown(<<'MARKDOWN', Text::Markdown::Discount::MKD_NOHEADER | Text::Markdown::Discount::MKD_NOPANTS | 0x02000000);
```
fenced code block
```
MARKDOWN
<pre><code>fenced code block
</code></pre>

MKD_FENCEDCODEフラグはDiscount処理系で定義されているものの、Text::Markdown::Discountモジュールでは定数が定義されていていないため、フラグの値である0x02000000を直接指定しています。(定数を定義するpull requestが提出されています。)

MKD_NOHEADERフラグおよびMKD_NOPANTSフラグはmarkdown関数の第2引数を省略したときにデフォルトで適用されるフラグなので、元の挙動を変えずに別のフラグを追加する際には、このふたつのフラグも明示的に指定する必要があります。

MKD_FENCEDCODEフラグを指定しない場合、3つ以上のバッククォートの並びはインラインのコード範囲として解釈されます。

my $html = markdown(<<'MARKDOWN');
```
fenced code block
```
MARKDOWN
<p><code>
fenced code block
</code></p>

Text::Markdown::Discount 0.13

特にフラグを指定しなくても、囲い付きコードブロックが有効になっています。

my $html = markdown(<<'MARKDOWN');
```
fenced code block
```
MARKDOWN
<pre><code>fenced code block
</code></pre>

Text::Markdown::Discount 0.12以前

囲い付きコードブロックに対応していません。3つ以上のバッククォートの並びはインラインのコード範囲として解釈されます。

CSSでスクロールバーの有無によるがたつきをなくす2022年12月09日 09時42分

この記事はCSS Advent Calendar 2022の9日目の分です。


CSSのボックスモデルにおいては、ボーダーの内辺とパディングの外辺の間にスクロールバーが配置されます。最近はどのOSでもオーバーレイスクロールバー(スクロールバーが内容の前面に覆いかぶさるようなもの)が主流となり、スクロールバーが存在してもしなくても内容の幅が変わらないようになっています。一方、クラシックスクロールバー(スクロールバーが常に表示されるようなもの)が使われる環境では、overflow: autoな要素において内容がはみ出すときとはみ出さないときで内容の幅が変わってきます。

クラシックスクロールバーが使われる環境でも内容の幅を一定に保ちたいという場合は、scrollbar-gutterプロパティを使います。scrollbar-gutter: stableを指定すれば、スクロールバーが表示されないときでもスクロールバーと同じだけの領域が確保され、内容の幅はその分狭くなります。左右中央に配置したいのにスクロールバーの領域の分だけずれて困るというときは、scrollbar-gutter: stable both-edgesを指定することで、左右どちらにもスクロールバーと同じだけの領域が確保されます。

scrollbar-gutterプロパティの各値を指定した例

scrollbar-gutterプロパティを指定した効果の図

(上図はWindows 10 Firefox nightly 109.0a1での表示)

scrollbar-gutterプロパティはoverflow: hiddenな要素にも適用されますが、overflow: visibleな要素には適用されません。また、scrollbar-gutterプロパティの効果は、縦スクロールバー(縦書きなら横スクロールバー)にのみ影響します。

オーバーレイスクロールバーが使われる環境では、scrollbar-gutter: stableの効果はありません。

使用事例

使用事例としては、@about_hiroppyさんの紹介している「HTMLのdialog要素によるモーダルダイアログが開いているときに、背景の文書をスクロールさせない」というのが考えられます(scrollbar-gutterプロパティを指定した例指定しなかった例)。

html {
  scrollbar-gutter: stable;
}
html:has(dialog:modal) {
  overflow: hidden;
}

クラシックスクロールバーが使われる環境では、文書をスクロールさせないためにoverflow: hiddenを指定すると、スクロールバーが消えて文書の内容ががたついてしまいます。scrollbar-gutter: stableをあらかじめ指定しておけば、スクロールバーが消えてもその分の領域が確保され続けるので、文書の内容ががたつきません。

Perlで配列の先頭何要素か以外を抜き出す2022年12月05日 01時47分

この記事はPerl Advent Calendar 2022の5日目の分です。


Perlで配列の先頭n要素以外を抜き出したい——例えば配列('a', 'b', 'c', 'd', 'e')から先頭2要素以外を抜き出して配列('c', 'd', 'e')を得たい——とき、最近はList::Utilモジュールtail関数を使えます。

tail関数は配列の末尾n要素を抜き出す関数ですが、抜き出す要素数として負数-mを指定すると、先頭m要素以外の要素を返します。

use List::Util qw(tail);
my @array1 = qw(a b c d e);
my @array2 = tail -2, @array1;
# @array2の内容は('c', 'd', 'e')

List::Utilモジュールはコアモジュール(Perl本体と一緒にインストールされるモジュール)であり、Perl 5.28以降なら追加のモジュールインストールなしにtail関数を使えます。それより古いPerlでは、List::Utilの新しいバージョン(1.50以降)をインストールする必要があります。

以前からある方法として、配列スライスを使うこともできます。

my @array1 = qw(a b c d e);
my @array2 = @array1[2 .. $#array1];
# @array2の内容は('c', 'd', 'e')

TypeScriptでイベントをPromise化する関数の型を定義したい2022年12月03日 10時41分

この記事はTypeScript Advent Calendar 2022の3日目の分です。


addEventListenerでリッスンしているイベントをPromise化する」という記事で、イベントをPromiseで受け取る関数が紹介されています。Node.jsのeventsモジュールのevents.onceメソッドと同じ機能を実現するものですね。Webブラウザ組み込みのDOMでも同じ機能を提供しようという提案もなされています。

最初の記事では余談として今回紹介したeventPromisifyはTypeScriptで書こうとすると型の定義が難しいなと思いましたと書かれています。例えばeventPromisify(document, 'click')と呼び出したら返り値の型がPromise<MouseEvent>になってほしいのですが、そのような型定義を記述できるでしょうか? (以下、TypeScript 4.9を想定しています。)

イベントが発生する対象(target)とイベント名(type)を決め打ちできるのなら、

type EventForDocumentClick =
  typeof document.addEventListener<'click'> extends
    (type: 'click', listener: (event: infer E) => void) => void
  ? E : never;
// = MouseEvent

のように、document'click'からMouseEventを導出できます。しかし、変数documentではなくDocument型だけが与えられているとき、Document['addEventListener']<'click'>のように型引数を指定することはできません。

またDocument型におけるaddEventListenerメソッドの定義は、イベント名がDocument固有(keyof DocumentEventMap型)のものとイベント名が文字列全般(string型)のものがオーバーロードされています。型引数が絡んでいなければ、「オーバーロードされた関数型から引数の型や返り値の型を取り出す方法」に書かれているように型を取り出せます。しかし、型引数を持つメソッドがオーバーロードされているときに、意図した型引数が指定された場合の引数や返り値の型を取り出せるのかどうか、私にはわかりませんでした。

結局私にできたのは以下の状態までです(TypeScript Playgroundで確認)。

type EventTypeFor<Target extends EventTarget> = Target['addEventListener'] extends {
  (type: infer T, listener: (e: Event) => void): void;
  (type: string, listener: EventListenerOrEventListenerObject): void;
} ? T : never;

type EventFor<Target extends EventTarget> = Target['addEventListener'] extends {
  (type: string, listener: (e: infer E) => void): void;
  (type: string, listener: EventListenerOrEventListenerObject): void;
} ? E : never;

const eventPromisify = <T extends EventTarget>(
  target: T,
  type: EventTypeFor<T>
): Promise<EventFor<T>> => {
  throw new Error('Not implemented');
}

const p = eventPromisify(document, 'click');
// p: Promise<Event | MouseEvent | UIEvent | ClipboardEvent | AnimationEvent | InputEvent | FocusEvent | ... 11 more ... | WheelEvent>

第2引数をDocument固有のイベント名に限定することはできています。しかしながら、返り値のPromiseの値の型をMouseEventに限定することはできず、Document固有のイベントすべての共用体型となってしまいます。

Perlで配列の先頭何要素かを抜き出す2022年12月02日 02時16分

この記事はPerl Advent Calendar 2022の2日目の分です。


Perlで配列の先頭n要素を抜き出したいとき、最近はList::Utilモジュールhead関数を使えます。

use List::Util qw(head);
my @array1 = qw(a b c d e);
my @array2 = head 3, @array1;
# @array2の内容は('a', 'b', 'c')

List::Utilモジュールはコアモジュール(Perl本体と一緒にインストールされるモジュール)であり、Perl 5.28以降なら追加のモジュールインストールなしにhead関数を使えます。それより古いPerlでは、List::Utilの新しいバージョン(1.50以降)をインストールする必要があります。

以前からある方法

配列スライスを使うこともできますが、抜き出す要素数から1引いた値を指定することになって、ちょっと紛らわしいです。

my @array1 = qw(a b c d e);
my @array2 = @array1[0 .. 2];
# @array2の内容は('a', 'b', 'c')

また、元の配列の要素数が抜き出す要素数より少ないときは、不足分がundefで埋められてしまいます。

my @array1 = qw(a);
my @array2 = @array1[0 .. 2];
# @array2の内容は('a', undef, undef)

undefで埋められたくなければ、min関数を使うなどひと工夫する必要があります。

use List::Util qw(min);
my @array1 = qw(a);
my @array2 = @array1[0 .. min(2, $#array1)];
# @array2の内容は('a')

splice関数を使うこともできますが、元の配列も変更されてしまいます。

my @array1 = qw(a b c d e);
my @array2 = splice @array1, 0, 3;
# @array1の内容は('d', 'e')
# @array2の内容は('a', 'b', 'c')