本記事ではVivliostyle用のMarkdownとして開発されている、VFM
MarkdownとGFM、VFMの関係
構造化された文書を記述するためのマークアップ言語としてHTML/
以下はHTMLとMarkdownで同じ文書を記述した比較例です。
MarkdownはHTML/
ただしMarkdownには標準仕様がありません。そのため方言が乱立することになりました。この課題を解決するためにCommonMarkが提案され、現在はこれをベースにしたGFM(GitHub Flavored Markdown)が広く普及しています。
ここでVFM (Vivliostyle Flavored Markdown)の登場です。
Vivliostyleは組版の構造定義にHTMLを採用しています。しかし前述のように人間が記述する難しさが課題でした。そこでこれを解決しつつ、書籍組版ができる豊富な表現力を持ったHTMLを生成するためにVFMが提案されました。開発はuetchyさんにより2020/
VFMを提案するにあたり、完全な新規方言とせずGFMをベースとすることにしました。広く普及しているGFMの知見を流用でき、そのうえで必要に応じて構文や機能を追加する方針としています。
VFM の拡張構文
VFMはGFMをベースに拡張構文を追加しています。その中でもとりわけ重要で特徴的なものを紹介します。
すべての構文について知りたい場合は以下を参照してください。
文書のメタデータ定義 - Frontmatter
Markdownは構造化された文書を記述できますが、文書そのもののメタデータを記述する構文がありません。Markdown単体で利用するなら問題ありませんが、HTML/
例えば著者の異なる複数のMarkdownがあり、それらから著者の索引を生成したいとします。その場合、Markdownとは別に著者を定義する仕組みが必要です。
この課題を解決するためにVFMはFrontmatterを提供します。---
と---
の区間にYAML形式でメタデータを記述可能です。以下の例では文書のタイトルと著者を定義しています。
---
title: "第2回 Vivliostyleに特化したMarkdown - VFMの使い方"
author: "akabeko"
---
これを HTML に変換すると以下のようになります。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>第2回 Vivliostyleに特化したMarkdown - VFMの使い方</title>
<meta name="author" content="akabeko" />
</head>
<body></body>
</html>
HTMLとしてメタデータが出力されるため、二次利用が容易になります。<head>
以外にも<html>
や<body>
の属性を定義できます。これを利用することで文書ごとに異なるスタイルを定義することも可能です。
Vivliostyle CLIには複数文書をMarkdownファイルとして分割し、それらを結合して印刷する機能があります。その際、Frontmatterは文書ごとに異なるメタデータを定義する手段として有用です。
他にも強力な機能があります。文書ごとのカスタマイズが必要な場合、まずはFrontmatterから検討することをお勧めします。
脚注 - Footnotes
FootnotesはPandocの脚注を踏襲した構文です。
VFMはGitHubリポジトリで開発しています[^1]
[^1]: [VFM](https://github.com/vivliostyle/vfm)
これは以下のHTMLに変換されます。
<p>
VFMはGitHubリポジトリで開発しています<a id="fnref1" href="#fn1" class="footnote-ref" role="doc-noteref"><sup>1</sup></a>
</p>
<section class="footnotes" role="doc-endnotes">
<hr />
<ol>
<li id="fn1" role="doc-endnote">
<a href="https://github.com/vivliostyle/vfm">VFM</a
><a href="#fnref1" class="footnote-back" role="doc-backlink">↩</a>
</li>
</ol>
</section>
HTMLとしては複雑ですが、文中の<a>
と脚注<section>
内の項目を相互リンクしているだけです。複数の脚注を考慮して、属性値の番号は1〜Nで自動採番されます。
数式 - Math equation
Math equationはMathJaxと組み合わせて数式を組版するための構文です。
MathJaxの数式はインラインとディスプレイの2種類です。VFMでは$
囲いならインライン数式、$$
をディスプレイ数式として扱います。囲われた区間にはTeX/
inline:$x = y$
display: $$1 + 1 = 2$$
これは以下のHTMLに変換されます。
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.9/MathJax.js?config=TeX-MML-AM_CHTML"></script>
</head>
<body>
<p>inline: <span class="math inline" data-math-typeset="true>"\(x = y\)</span></p>
<p>display: <span class="math display" data-math-typeset="true">$$1 + 1 = 2$$</span></p>
</body>
</html>
VFMでは標準でMath equationが有効化されており、文書内にこの記法を検出するとCDN配布されたMathJax参照<script>
も出力します。Math equationが不要な場合はFrontmatterのvfm/
で無効化してください。
---
vfm:
math: false
---
この機能は数式を含む論文執筆などに役立つでしょう。基本的な文章はMarkdown、数式が必要になったら部分的にTeX/
ルビ - Ruby
RubyはHTMLの<ruby>
要素を記述するための構文です。Markdown方言としてもいくつか先行例があり、VFMではでんでんマークダウンの構文を参考にしました。
This is {Ruby|ルビ}
これは以下のHTMLに変換されます。
This is <ruby>Ruby<rt>ルビ</rt></ruby>
ルビは意外に利用したくなるものです。特に日本語は表意文字である漢字を多用するため、適切な表音を伝えるためにも読み仮名の併記は有用です。
HTML埋め込み - Raw HTML
GFM+VFM拡張構文でも表現力が足りない場合、HTMLをそのまま埋め込めます。
<div class="custom">
<p>Hey</p>
</div>
VFMによるHTML埋め込み処理はブロック単位です。そのため空行を挟むことでMarkdownと組み合わせられます。
<div class="custom">
# Heading
</div>
これは以下のHTMLに変換されます。
<div class="custom">
<section class="level1" aria-labelledby="heading">
<h1 id="heading">Heading</h1>
</section>
</div>
よくある利用例としては<table>
埋め込みがあります。Markdown のテーブル構文がサポートするのは簡素なヘッダーとボディ構造だけです。そのため<th>
を<tbody>
側に配置したり、rowspan
やcolspan
による結合を指定できません。このような場合、HTML埋め込みを利用することで<table>
そのままの表現力を持たせられます。
セクション分け - Sectionization
見出しのレベルを基準に、階層化されたセクションを生成します。
- 見出しのレベル
(見出し行の先頭の #
の数)に応じてセクションを分けて階層化します - 見出しの行が
#
ではじまり、同数以上の#
で終わる場合はセクションを分けません### Not Sectionize ###
(同じ数の #
で囲まれている)の場合、セクションを分けません ### Sectionize ##
(閉じの #
の数が足りない)の場合、セクションを分けます
#
だけからなる行により、#
の数と一致するレベルのセクションを終了させます- 例:
### Heading 3
で開始したセクションは###
で終了させます
- 例:
- 親が
blockquote
の場合はセクションを分けません - 見出しのレベルへ一致するように、セクションの
levelN
クラスを設定します - 見出しの
id
属性値をセクションのaria-labelledby
属性へ値をコピーします- この機能はアクセシビリティーの観点から廃止を検討しています
- 詳細はaria-labelledby should not be output in every sectionを参照してください
複雑なルールに見えますが、基本的には文書構造の階層にそった見出しを書くことで、そのままセクション分けと階層化がおこなわれます。
# Plain
# Introduction {#intro}
# Welcome {.title}
# Level 1
## Level 2
### Level 3
##
Level 2 was ended by `##`.
## Not Sectionize {.just-a-heading} ##
> # Not Sectionize in Blockquote
これは以下のHTMLに変換されます。
<section class="level1" aria-labelledby="plain">
<h1 id="plain">Plain</h1>
</section>
<section class="level1" aria-labelledby="intro">
<h1 id="intro">Introduction</h1>
</section>
<section class="level1" aria-labelledby="welcome">
<h1 class="title" id="welcome">Welcome</h1>
</section>
<section class="level1" aria-labelledby="level-1">
<h1 id="level-1">Level 1</h1>
<section class="level2" aria-labelledby="level-2">
<h2 id="level-2">Level 2</h2>
<section class="level3" aria-labelledby="level-3">
<h3 id="level-3">Level 3</h3>
</section>
</section>
<p>Level 2 was ended by <code>##</code>.</p>
<h2 class="just-a-heading" id="not-sectionize">Not Sectionize</h2>
<blockquote>
<h1 id="not-sectionize-in-blockquote">Not Sectionize in Blockquote</h1>
</blockquote>
</section>
見出しのレベルと文書の階層構造を一致させ、HTMLの<section>
として階層化されます。CSSクラスとしてもレベルが指定されるため、階層ごとにスタイルを定義しやすくなります。
以下はHTMLサンプルに対するCSSセレクターの定義例です。
body > section {
}
section:has(> #intro) {
}
section:has(> h1.title) {
}
.level1 {
}
.level2 {
}
blockquote > h1 {
}
書籍の組版では章節項のような階層構造がよく登場します。これらには個別のスタイルを定義したくなるものです。例えば章から節、項へと下るにつれて文字サイズや配色、インデントの強弱を調整することがあります。VFMのセクション分けはこのような要求に応えるための機能です。
VFMの利用方法
VFMはVivliostyle CLI経由で間接的に利用する方法と、Node.
間接利用については以下を参照してください。Create Bookで作成されたプロジェクトにはVivliostyle CLIとVFMが含まれ、CLIが自動的にVFMによる変換を実行します。
本記事では普段、あまり触れられることのない直接利用について解説します。VFMはNode.
VFMが提供する機能としてはCLIとAPIがあります。
CLI
CLIを試したい場合はNode.
$ npm install -g @vivliostyle/vfm
これでターミナルからvfm
コマンドが利用可能になります。試しに以下を実行してvfm
のヘルプ表示を確認してみてください。
$ vfm --help
vfm
コマンドのデータ入力形式はファイルと文字列に対応しています。ファイルの場合は引数にパスを指定してください。
$ vfm index.md
文字列は標準入力から渡します。
$ echo "# Hello" | vfm
これらを実行すると標準出力にHTMLが出力されます。
CLIは|
で結果を他のコマンドに渡したり、>
によりファイルへ書き出すことも可能です。既存のCLIツールやシェルスクリプトにMarkdown→HTML変換を組み込みたくなった際、利用してみてください。
API
APIを利用したい場合はNode.
$ npm install @vivliostyle/vfm
MarkdownをHTML文字列に変換したい場合はstringify
関数を利用します。
import { stringify } from "@vivliostyle/vfm";
console.log(
stringify(`
# はじめに
{Vivliostyle|ビブリオスタイル}の世界へようこそ。
`)
);
このスクリプトを実行すると、変換されたHTMLが標準出力に表示されます。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>はじめに</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<section class="level1" aria-labelledby="はじめに">
<h1 id="はじめに">はじめに</h1>
<p>
<ruby>Vivliostyle<rt>ビブリオスタイル</rt></ruby>の世界へようこそ。
</p>
</section>
</body>
</html>
stringify
関数は内部でVFM
関数を呼び出しています。
/**
* Convert markdown to a stringify (HTML).
* @param markdownString Markdown string.
* @param options Options.
* @returns HTML string.
*/
export function stringify(
markdownString: string,
options: StringifyMarkdownOptions = {},
metadata: Metadata = readMetadata(markdownString)
): string {
const processor = VFM(options, metadata);
const vfile = processor.processSync(markdownString);
debug(vfile.data);
return String(vfile);
}
stringify
関数を呼び出すとunifiedのProcessor
を返します。VFMを更に拡張して構文を追加したい場合、Processor
のuse
メソッドを利用してください。
VFMの公開関数としてもうひとつ、Markdownのメタデータを読むためにreadMetadata
関数を提供しています。試しに本記事のFrontmatterサンプルを読み込んでみましょう。
import { readMetadata } from "@vivliostyle/vfm";
console.dir(
readMetadata(
`
---
title: "第2回 Vivliostyleに特化したMarkdown - VFMの使い方"
author: "akabeko"
---
`
),
{ depth: null }
);
すると以下の出力を得られます。
{
title: '第2回 Vivliostyleに特化したMarkdown - VFMの使い方',
meta: [
[
{ name: 'name', value: 'author' },
{ name: 'content', value: 'akabeko' }
]
]
}
メタデータは文書横断の処理に役立ちます。例えば索引や奥付の生成、独自にmeta
として定義した値で条件分岐するなどが考えられます。ブログを自作する際に記事のカテゴリーやタグを定義して一覧ページ生成に応用するのもよさそうです。
また、メタデータはstringify
関数の第3引数として指定可能です。そのためstringify
によるHTML変換をカスタマイズできます。
VFMの設計と課題
記事の締めくくりとして、VFMの設計と課題について解説します。設計に興味がある、またはVFMの開発に参加したい場合は参考にしてください。
AST処理
VFMのMarkdownとHTML処理はunifiedに基づきます。unifiedは様々な言語処理を統一されたAST
# Title
ASTでは見出しを示す行頭の#
は不要なので取り除かれ、代わりに構文処理として便利な情報が追加されます。
{
type: "heading",
depth: 1,
children: [{ type: "text", value: "Title" }]
}
unifiedをNode.
- remarkjs/
remark - MarkdownをMDAST
(MarkdownのAST) として処理 - GFMやFrontmatterなどもremrakプラグインを利用
- MarkdownをMDAST
- remarkjs/
remark-rehype - MDASTをHAST
(HTMLのAST) に変換して処理 - remarkと組み合わせて利用
- MDASTをHAST
- uniste
- unist = Universal Syntax Tree
- MDASTやHAST操作を便利にするためのライブラリー群
- 処理対象のASTから特定の構文を探して置換するなどの機能を提供
remarkはMarkdownの拡張構文を追加するためにプラグイン機能を提供しており、VFMでもそれに基づき実装された構文があります。しかしこの方法には課題もあります。
VFM の設計的な課題
unifiedとremarkを組み合わせて利用する場合、Markdownの構文拡張は以下に大別されます。
- remarkプラグインとして実装
- unifiedとしてMDAST処理を自前で実装
VFMでは基本的に1を採用しています。しかしVFMのそれはremark 12までの設計となっており13以降には対応していません。remark 13からMarkdwon解析エンジンが刷新されており、結果としてremarkプラグインの互換性もなくなりました。プラグインのインターフェース変更も相当に大きく、再設計が必要なレベルです。そのため開発リソースの限られているVFMとしては追従が難しい状況となっています。
この課題について、構文拡張を前述の方針2へ転換することで解決を図ろうとしています。remarkはそれ自体が提供する機能と標準プラグイン利用に限定し、remarkプラグインとして実装した構文拡張はMDASTを自前で処理します。
例えば本記事でVFM構文として紹介したルビ。これはremarkプラグインとして実装されていますが、MDASTの見出しや段落のvalueを処理してルビを入れ子にします。大まかには以下のようになるでしょう。MDASTとしての処理なので、remarkへ依存しなくて済みます。
- MDASTから
type: heading
などを探す children
からtype: "text"
を探すvalue
からルビ構文を探すchildren
を[ルビ検出直前text, ルビ, ルビ検出以降text]
に調整
VFMは開発者を常に募集中です。本記事で興味を持たれた方、以下のリポジトリでお待ちしています!