このウェブサイトでは、画像や引用をfigure要素でマークアップしている。Markdownで書いているわけだが、もちろんそんな記法はないので、今までは生HTMLを書いていた。しかし、marked v0.4.0でのバグでハマりどうしようもなさそうだったため、markedの拡張機構を使って、なんとなくうまい感じになるようにする必要がでてきてしまった。

現在のHTML仕様ではp要素が柔軟に使えるので、わざわざfigure要素を使うべき、というわけではない。しかしMarkdownだとクラス名を自由に振れるわけではないので、普通の段落と画像だけの段落を区別できない。Selectors Level 4が自由に使えるようになるまでは、スタイル都合で画像や引用と普通の段落を明示的に区別したいという気持ちが強い。

画像を括るfigure要素

想定するパターンはキャプションのあるなしの2つだ。キャプションの記述には、Markdownのいくつかの記法にあるtitle属性を流用するようにした(使ったことがない)。画像記法のそれだと実装が面倒くさかったため、リンクを張ることを前提に、ハイパーリンク記法のそれを利用する。

[![alt](src)](href "caption")

[![alt](src)](href)

このMarkdownから以下のHTMLを生成したい。

<figure>
<a href="href"><img alt="alt" src="src"></a>
<figcaption>caption</figcaption>
</figure>

<figure>
<a href="href"><img alt="alt" src="src"></a>
</figure>

実装は2つのフェーズに分けて行った。リンク側ではキャプションがあり、かつ内容が画像だったら、figcaption要素でキャプションをマークアップして追加する。段落側では、リンクが張られた画像だけか、それに加えてfigcaption要素があるなら、全体をp要素ではなくfigure要素でマークアップする。

const markupHyperlink = (href, title, content) => {
  if (!title) {
    return `<a href="${href}">${content}</a>`;
  }

  if (!content.startsWith("<img ")) {
    return `<a href="${href}" title="${title}>${content}</a>`;
  }

  return `<a href="${href}">${content}</a>
<figcaption>${title}</figcaption>`;
};

const markupParagaraph = content => {
  if (!/^<a\b.*?><img\b.*?><\/a>(\n<(figcaption)>.*?<\/\1>)?$/.exec(content)) {
    return `<p>${content}</p>
`;
  }

  return `<figure>
${content}
</figure>
`;
};

引用を括るfigure要素

画像とは違い、引用元の表記があるかないかで分ける。流用出来そうな記法はないので、最終行が(EMダッシュ)で始まっていた場合は引用元とみなす、ということにする。そうでない場合はfigure要素でマークアップしなくても良いので、何もしない。

> This is a quote.
>
> — <cite>[text](href)</cite>

> This is a quote.

— <cite>[text](href)</cite>

> This is a quote.
>
> This is a quote.

最初のみfigure要素でマークアップし、以下のHTMLを生成したい。

<figure>
<blockquote>
<p>This is a quote.</p>
</blockquote>
<figcaption>— <cite><a href="href">text</a></cite></figcaption>
</figure>

<blockquote>
<p>This is a quote.</p>
</blockquote>

<p>— <cite><a href="href">text</a></cite></p>

<blockquote>
<p>This is a quote.</p>
<p>This is a quote.</p>
</blockquote>

実装は引用内の最後の行のみをチェックして行った。EMダッシュで始まっていないなら通常通り出力する。始まっていた場合は最後の行をp要素からfigcaption要素へ変換し、さらにfigure要素で全体をマークアップする。

const markupQuote = content => {
  const lines = content.trim().split("\n");

  if (!lines[lines.length - 1].startsWith("<p>—")) {
    return `<blockquote>
${lines.join("\n")}
</blockquote>
`;
  }

  lines.push(lines.pop().replace(/<(\/)?p>/g, "<$1figcaption>"));
  return `<figure>
<blockquote>
${lines.join("\n")}
</blockquote>
</figure>
`;
};

もうちょっと特別なルールをなくしたい。画像の方はリンクを強要したくないし、引用と同じようにキャプションのあるなしで分けた方が良いかもしれない。EMダッシュはそこそこ書きやすいので、引用はこのような形でベストかもしれない。とにかくなんとなくで書いたら、スカッとしたHTMLになってほしい。

また、コードから正規表現をなくしたいところだが、それは難しそうだ。arr.push(arr.pop())とやっているところもどうにかしたい。