niszetの日記

細かい情報を載せていくブログ

(Pandoc)BulletListの挙動についてメモ

仕様を理解すれば怖いことはない(はず)

なんかタイトルが記事内容とあってないので変えました…。元は Pandoc 2.7以前/以降でBulletListの挙動が異なるので注意 でした。が、実際2.7より前では挙動が違うので、この記事の結果と違う挙動のはずです。

なお、この話はMarkdown Readerの仕様です。他の記法において同じルールが適用できるかは未調査です。

ちょっと込み入った話になりますが、込み入ってる分わかりづらいので書き留めておきます。

発端はいつもの?K4氏の発言から…

バージョン

まず、この仕様はPandoc2.7以降の挙動です。更新履歴では Markdown Reader の "Improve tight/loose list handling (#5285). Previously the algorithm allowed list items with a mix of Para and Plain, which is never wanted." が相当します。

github.com

今回はPandocのバージョンは2.10.1ですが、2.7以降であれば同じ挙動のはずです。

$ pandoc --version
pandoc.exe 2.10.1
Compiled with pandoc-types 1.21, texmath 0.12.0.2, skylighting 0.8.5
Default user data directory: C:\Users\nisze\AppData\Roaming\pandoc
Copyright (C) 2006-2020 John MacFarlane
Web:  https://pandoc.org
This is free software; see the source for copying conditions.
There is no warranty, not even for merchantability or fitness
for a particular purpose.

実行例

最初に使用する例はこれです。

- para1
- para2
  - para2.1

  para2.2
- para3
  - para3.1
  - para3.2
- para4

  para5

これを、適当にhoge.mdなどの名前で保存したとして、PandocへのPATHが通っているとして、

pandoc -t native hoge.md

結果は次のようになります。

[BulletList
 [[Para [Str "para1"]]
 ,[Para [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]]
  ,Para [Str "para2.2"]]
 ,[Para [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]
 ,[Para [Str "para4"]
  ,Para [Str "para5"]]]]

これではぱっと見よくわからないので、 -t html した結果も貼っておきます。

f:id:niszet:20200812001913p:plain

これを見ればわかりますが、para2.2とpara5は頭に・がつきません(記法的にも正しいですね)

また、細かい挙動はまた書きますが、この時点ではレベル1のリスト(一番左側にあるリスト)の要素(●)はParaに、インデントがついた、レベル2のリスト要素(○)はPlainになっていることが見れます。

また、元のマークダウン上ではわかりにくいですが、para2.2とpara5は先頭にホワイトスペースが2文字入っています。

-t htmlに変更して、出力されているhtmlを確認すると、

<ul>
<li><p>para1</p></li>
<li><p>para2</p>
<ul>
<li>para2.1</li>
</ul>
<p>para2.2</p></li>
<li><p>para3</p>
<ul>
<li>para3.1</li>
<li>para3.2</li>
</ul></li>
<li><p>para4</p>
<p>para5</p></li>
</ul>

このようになります。つまり、Paraのところは素直に

タグが挿入されていて、Plainの場合はそれがない、という違いです。

さて、この違い、LooseとTightのリスト(それぞれ、ParaとPlainに相当)ですが、これはBulletListの要素間に空行を入れているかどうかで決まります。

シンプルな例では、

- para1
- para2
  - para2.1

は、要素間が詰まっている(空行がない)のでTightリスト、つまりPlainが使われ、

[BulletList
 [[Plain [Str "para1"]]
 ,[Plain [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]]]]]

各要素間に空行を入れると、Looseリストになります(つまり、Paraが使われる)

- para1

- para2

  - para2.1

変換すると、

[BulletList
 [[Para [Str "para1"]]
 ,[Para [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]]]]]

ここで、para2.1の方はParaではなくPlainになっている点に注意。これは、「同レベルの要素間」に空行が入らないとParaにはならないためです。また、次の例で確認できますが、このレベル2のリスト間は独立しています。

- para1
- para2
  - para2.1

  - para2.2
- para3
  - para3.1
  - para3.2

結果は次のようになります。

[BulletList
 [[Plain [Str "para1"]]
 ,[Plain [Str "para2"]
  ,BulletList
   [[Para [Str "para2.1"]]
   ,[Para [Str "para2.2"]]]]
 ,[Plain [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]]]

para2.1/2.2の方は空行の影響を受けてParaとなりますが、para3.1/3.2は空行を持っていないのでPlainとなり、それぞれ独立していることがわかります。一方、トップレベルは共通しているので、どこかに空行があれば影響をうけます。

- para1
- para2
  - para2.1
  - para2.2
- para3

  - para3.1
  - para3.2

結果は次のように。

[BulletList
 [[Para [Str "para1"]]
 ,[Para [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]
   ,[Plain [Str "para2.2"]]]]
 ,[Para [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]]]

さて、最初の例に戻ります(再掲)。これ

- para1
- para2
  - para2.1

  para2.2
- para3
  - para3.1
  - para3.2
- para4

  para5

を変形すると、以下のようになりました。

[BulletList
 [[Para [Str "para1"]]
 ,[Para [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]]
  ,Para [Str "para2.2"]]
 ,[Para [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]
 ,[Para [Str "para4"]
  ,Para [Str "para5"]]]]

まずレベル2の要素を見るとPlainです。3.1/3.2は明らかで良いですが、2.1の方は少し不思議ですね。これは、2.2はリストの〇を持ってないことから、リストの要素としてみなされていないようです。ただし、この行は先頭にホワイトスペースが2つあり、リストの中に入っています(nativeのASTより明らかですが)

これを取り払うとリストに含まれないと扱われます。その場合、以降が文字列と扱われるので、後半のリストをリストのまま扱いたい場合はpara2.2の上下に空行を入れる必要があります。

- para1
- para2
  - para2.1
  
para2.2

- para3
  - para3.1
  - para3.2
- para4

  para5

結果として、para2.2は独立した段落となり、上下のリストは別のリストとなります。これは、リストを区切りたいときに必要となるため覚えておくと良いでしょう。なお、リストを分割したいが、間に文字を入れたくない場合は、コメント行を入れるなど表示されないが段落として扱ってくれる文字列を入れることで対応します。

[BulletList
 [[Plain [Str "para1"]]
 ,[Plain [Str "para2"]
  ,BulletList
   [[Plain [Str "para2.1"]]]]]
,Para [Str "para2.2"]
,BulletList
 [[Para [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]
 ,[Para [Str "para4"]
  ,Para [Str "para5"]]]]

残るはpara5の?挙動です。この行は先頭にホワイトスペースが2つあるのでリストの中に取り込まれます(ASTより明らか)

この行を取り払う、あるいはpara4との間の空行を取り払うとトップレベルはPlainになります。

実は、ここまで書いてから気づきましたが、最初のpara2.2の箇所は行頭に置くべきはホワイトスペース4つで、para2.1とインデントを揃える必要がありました。

- para1
- para2
  - para2.1
  
    para2.2
- para3
  - para3.1
  - para3.2
- para4

  para5

すると、

[BulletList
 [[Para [Str "para1"]]
 ,[Para [Str "para2"]
  ,BulletList
   [[Para [Str "para2.1"]
    ,Para [Str "para2.2"]]]]
 ,[Para [Str "para3"]
  ,BulletList
   [[Plain [Str "para3.1"]]
   ,[Plain [Str "para3.2"]]]]
 ,[Para [Str "para4"]
  ,Para [Str "para5"]]]]

のようになり、para2.1もParaの対象となります。これは、ホワイトスペースを追加したことでpara2.2が正しくレベル2の要素として扱われたためです。これはpara4とpara5の関係と同じで(para5も行頭に2つホワイトスペースがあるから)、この挙動はリストのレベルによって変わらないことがわかります。

まとめると…

リストのアイテム(先頭に-がついているもの)の次の行(正確には空行はいくらあっても良い)が空行であり、その後同レベルの要素が続く場合、そのリストのアイテムのレベルはLoose扱いとなる。これは1つでも空行があればその要素群全体が対象です。

いくつか例を。以下の例ではpara3.4の後に空行がありますが、その後に来るのは同レベルの要素ではないのでPlainになります(最初のpara2.1の例と同じ。同レベルではないため)

- para3
  - para3.1
  - para3.2
  - para3.3
  - para3.4
  
- para4

また、次のように間の空行がいくつあっても良いです。para3.1-3.4はParaとなり、空行は出力時には消えます。そのため、リストの要素間で明示的にスペースをあけるようには使えません(そもそも項目の列挙をする際に、そのような配置をしてはいけないですが)

- para3
  - para3.1
  - para3.2
  
  
  
  
  
  - para3.3
  - para3.4
  
- para4

…ということで、私も途中まで勘違いしている個所がありましたが、PandocのLoose/Tight Listの挙動について簡単にまとめました。インデントでリストのレベルを制御するのはちょっとわかりづらいですね(para2.1/2.2の例を思い浮かべながら)

もうちょっとシュッと説明できる気がしますが、折角書いたのでこのままとし、今後の課題とします。