XSL入門 0 CommonMark文書のDTDを雰囲気で読む

7 min read読了の目安(約6300字

この記事はXSL Advent Calendar 2020の7日目の記事です。追記して8日目の記事でもある。

https://adventar.org/calendars/5027

ついでにMarkdown Advent Calendar 2020に登録するかと思ったけど8日目は埋まってますね。頑張ってほしい。

D・T・D! D・T・D!

タイトルの元ネタ知ってる人は黙っていようね。

CommonMarkのASTがXML出力できる話は過去回で話しました。キチンとしたXMLアプリケーションは構造を定義した文書型を持っています。SGMLから引き継いだ文書型がDTDで、XML用の新しいのがXMLSchema、こんなん書いてられるかと登場したのがRELAXNG、というのは雑すぎる紹介ですが。

ネームスペースとして宣言するときはhttp://commonmark.org/xml/1.0なんですが、この1.0は何の1.0なんだろう。

https://github.com/commonmark/commonmark-spec/blob/master/CommonMark.dtd

それではCommonMarkのDTDを、知識を0にした状態から読んでいきましょう。

ブロックとインライン、それからルート要素

<!ENTITY % block         'block_quote|list|code_block|paragraph|heading|thematic_break|html_block|custom_block'>
<!ENTITY % inline
'text|softbreak|linebreak|code|emph|strong|link|image|html_inline|custom_inline'>

<!ELEMENT document (%block;)*>
<!ATTLIST document
    xmlns CDATA #FIXED "http://commonmark.org/xml/1.0">

CommonMark.dtd冒頭です。知識ほぼ0で見ても何となく「あーなるほどそういうことね」と理解できそうな気がする。

blockinlineというエンティティがあり、blockに分類される何かにはblock_quotelist、……というものがある、ということが書かれています。「エンティティって何やねん」というのは後回しです。

そしてその後、documentエレメントがあります。%block;*、先刻のblockのことですね。こいつが()*の中ということで、「0個以上のblockdocumentの中に書けるよ」と。ははあ。

documentエレメントの下、ATTLISTがあります。ELEMENTと来てATTLIST、しかも直ぐ上に続いてdocumentが書かれていますからね。これは属性(attribute)のリストですね。xmlns属性を
持てることはわかる。
さすがにCDATA#FIXEDは察し力だけでは限界があります。これも後で調べるリストに入れておきましょう。

引用ブロック

<!ELEMENT block_quote (%block;)*>

block_quoteエレメントがここで登場しました。またblockが登場しているが、block_quoteblockなのでは……?と思った方、間違っていません。Markdownのブロック引用を見てみましょう。

https://commonmark.org/help/tutorial/05-blockquotes.html
> First line
> Another line
>
> > Nested line

はい。block_quoteは入れ子可能なんですね。先刻はエンティティとしてまとめてあったという話で、要素としてはここで記述されているワケだ。

リスト

<!ELEMENT list (item)+>
<!ATTLIST list
          type (bullet|ordered) #REQUIRED
          start CDATA #IMPLIED
          tight (true|false) #REQUIRED
          delimiter (period|paren) #IMPLIED>

<!ELEMENT item (%block;)*>

ASTとしての複雑さはCommonMarkで1番か2番なんじゃないか? XSL-FOでも結構手間で、リスト構造が原理的に難しいのではと思わせます。list構造はitem子要素を1つ以上持ち、4つ属性がある、成程。で、その属性が取れる値がその後ろの組み合わせということですね。bulletorderedtruefalseが出てきたら察せられてくる。まあ、そうなってくるとCommonMarkを雰囲気で書いている人間としては「tight is 何」みたいな疑問が湧いてきそうですね。

delimiterperiodparen、つまり「.」と「)」。これは数字付きリストのときに付くヤツで、箇条書きリストでは要りませんね。#IMPLIED、お前は必ずしも必要でないということやな。
#REQUIREDも察しがついてきますよね。属性の属性……先に書いてしまうと用語としてはこれは「特性」と呼ばれるのです。

itemblockを子に持てるんですね。

コードブロック

<!ELEMENT code_block (#PCDATA)>
<!ATTLIST code_block
          xml:space CDATA #FIXED "preserve"
          info CDATA #IMPLIED>

code_block、この中身は変に解釈されてしまうと困るワケで、そうすると子に持てるのはRAWな記述ですよね。ということで#PCDATAは「文字データ」を表します。エスパー度合いが上がりますが、コードブロックではスペースも処理してまとめられたりすると困るのでxml:spacepreserveに固定される値なんだな、#FIXEDは固定値なんだな、と薄ら思ってたことを確定させます。「#PCDATAが生文字列なんだからCDATAも任意の文字列を表す型なのでは……?」はい、ちょっと面倒になってきたのでこれも確定させましょう。

段落

<!ELEMENT paragraph (%inline;)*>

考えることがなくなってきて安心しますね。paragraphの子にはinlineが0以上持てます、と。

見出し

<!ELEMENT heading (%inline;)*>
<!ATTLIST heading
          level (1|2|3|4|5|6) #REQUIRED>

headingは無限のinlineが持てる。「えっ1つじゃなくて?」と思うかもしれませんが、ちょっと書いてみましょう。

# **This** is *Heading*

確かに1つに絞られると困るかも。

そしてlevel属性は1-6の数値を持ちます。<h1>とか使うどこぞのSGML派生言語より好感が持てる。

主題分割

いざ訳そうとすると困るやつ。HTMLにするとHORIZONTAL RULE<hr />で表わされたりしますが。
私的にはセクショニングのマークアップがないので代用に使うこともあるものの、どこまでをぶったぎったのかが分からないのでHTMLタグ書いた方が楽だったりもする。

<!ELEMENT thematic_break  EMPTY>

EMPTY。まあ空要素ってことですね。中が空でもthematic_break自体はblockです。絶対改行させるマンではあるからね。

HTMLブロック

<!ELEMENT html_block (#PCDATA)>
<!ATTLIST html_block
          xml:space CDATA #FIXED "preserve">

見れば分かる通り、子要素は#PCDATA扱い。ていうか一番外側のタグ名も持たないってマジ? まあ処理を考えるとそうなるけれども。HTMLタグの内部でMarkdown記法を使っても解釈されないのはASTからも明らかになってしまった。

カスタムブロック

<!ELEMENT custom_block (%inline;|%block;|item)*>
<!ATTLIST custom_block
          on_enter CDATA #IMPLIED
          on_exit CDATA #IMPLIED>

「えっCommonMarkもカスタムブロックを持っていいのか?」「ああ…どんどん使え」
on_enteron_exitも保持していいだなんて……。commonmark.jsやcmarkが処理してくれるかどうかとASTは関係ないしね。


ここからはinlineわよ。

テキスト

<!ELEMENT text (#PCDATA)>

ただのテキスト。XML表現としては<text>ただのテキスト</text>のようになる。

改行

<!ELEMENT softbreak EMPTY>
<!ELEMENT linebreak EMPTY>

thematic_breakblockだったけどsoftbreaklinebreakinline。まあ<br />とか考えればそうだよね。

コード

<!ELEMENT code (#PCDATA)>
<!ATTLIST code
          xml:space CDATA #FIXED "preserve">

textとAST上はほとんど同じに見えるcodeだけど、#FIXEDで入るxml:space="preserve"によりスペースを処理しない強い志を持っている。

強調

<!ELEMENT emph (%inline;)*>
<!ELEMENT strong (%inline;)*>

分かっておられるかと思いますが強調はinlineなんです。「段落を強調したいんだよな」みたいなことを考えたときは段落をマークアップした上で、子のテキストをinlineとして強調するのがスジ。

リンク

DTDをペタペタしながら感想文張ってるの何なんだろうね。おかしい、書き始めたときはこんな内容になるはずでは。

<!ELEMENT link (%inline;)*>
<!ATTLIST link
          destination CDATA #REQUIRED
          title CDATA #IMPLIED>

子のinlineはいいとして、リンク先を格納するdistinationとリンクタイトルを示すtitleが属性。titleってリンクテキストじゃなく持てるんですよ。生でHTML書くときは気をつけたいね。

イメェジ

<!ELEMENT image (%inline;)*>
<!ATTLIST image
          destination CDATA #REQUIRED
          title CDATA #IMPLIED>

始めてマークアップを覚えるとき、画像はブロックだと思ってませんでしたか? ひだるまはインラインとかブロックについて考えてもいませんでした。linkとAST構造的には同じなんですよね。このdistinationはHTMLの<img>では@srcになります。

インラインHTML

<!ELEMENT html_inline (#PCDATA)>
<!ATTLIST html_inline
          xml:space CDATA #FIXED "preserve">

html_block同様、一番外側のタグ名も持っちゃいねえ。どうやって自分がブロックのHTML要素なのかインラインHTML要素なのか見分ける手段はASTは持たないってことです。ホントか?

カスタムインライン

<!ELEMENT custom_inline (%inline;)*>
<!ATTLIST custom_inline
          on_enter CDATA #IMPLIED
          on_exit CDATA #IMPLIED>

custom_inlineの子には流石にblockblockを持ち得るitemは記述できないようです。


POS

<!-- All elements can have a sourcepos attribute.
     The format is SL:SC-EL:EC
     where SL = number of starting line (first line = 1)
           EL = number of ending line
           SC = number of starting column (first column = 1)
           EC = number of ending column
-->
<!ATTLIST ANY sourcepos CDATA #IMPLIED>

最後に何か残っていました。

All elements can have ...

とあるので突然のATTLISTに`ANY``とか新キャラが出てきても大丈夫。全要素に適用されるってことですね。いいのかそんな書き方許して。行と何文字目かを持てる……ASTじゃなくてCSTか? 元テキスト完全再現は無理なんでそうでもないか。

先行資料

https://blog.antenna.co.jp/ILSoft2/archives/11632