📌

「あなたを、XMLです」 Invisible XML処理系 CoffeePot

2022/10/16に公開

2022年6月に、Invisible XML 1.0仕様がW3CのInvisible XML Community Groupより公開されました。Invisible XMLパーサCoffeepotは現在バージョン1.99.11。XProc 3.0が愈々大詰めというところで、Additional StepにInvisible XMLが追加され、XMLCalabash用のエクステンションも実装が進んでいます[1]。2022年のXML PragueでもInvisible XMLのチュートリアルがあるなど、XML界の若獅子といえるかもしれません[2]

https://github.com/nineml/coffeepot

と書いても大体の人が何も分からんと思うので、Invisible XMLとCoffeepotの紹介をする記事となります。

Invisible XML(ixml)

扨、「猫も杓子もXML」というXML万能説の夢が破れて何年経つでしょうか。この夢の一因には、「実際多くのケースで扱われる文書をXMLで表現できた」ということがあると思います。XMLまで持ってこなくとも処理できるケースが大半であったり、XML学習のコストであったり、数GBのテキストファイルを持ってきて「遅い」と言われたり、インターネット世界がそれなりに早くなるまでに2000年代前半から更に時間を要したことなどあったでしょう。しかし、ウェッブのケースの上層でXMLが使われなくなったからといって、抽象的形式としてのXMLの利点が損なわれた訳ではありません。

また、近年はXMLより簡単に書ける各種言語が人気です。人気のあまりJSON SchemaやCUE言語など、難解な仕組みを発明して相互運用性を上げる試みがされていますね。また新しい言語が流行れば新しい周辺ツールが登場するでしょう。

ところで、そこそこ資源が投入された汎用的マークアップ処理の仕組みがあってですね。この資産使えないかな?

一部誇張して表現しましたが、まあそんな感じのW3C Community Groupがあります(上のは翻訳ではないのであしからず)。

https://www.w3.org/community/ixml/

グループの趣旨は次のようなものです。

We choose which representations of our data to use, JSON, CSV, XML, or whatever, depending on habit, convenience, or the context we want to use that data in. On the other hand, having an interoperable generic toolchain such as that provided by XML to process data is of immense value. How do we resolve the conflicting requirements of convenience, habit, and context, and still enable a generic toolchain? How do we resolve the conflicting requirements of convenience, habit, and context, and still enable a generic toolchain? Invisible XML (ixml) is a method for treating non-XML documents as if they were XML, enabling authors to write documents and data in a format they prefer while providing XML for processes that are more effective with XML content.

詳細は後ろに譲るとして、簡単にまとめると「非XML文書をXMLとしてパースする仕組みを作ろう、それを使えば、バリデータやXSLTなどのXMLツール資産が何にでも使えて良いよね!」という話です。

コミュニティグループの発足は2021年となっていますが、原案は2013年頃まで遡れるようです。
Invisible XML Steven Pemberton, CWI, Amsterdam

「非XMLの文書だけれどXMLに対応する表現がある」という存在の魁としては、先のページにも紹介されている通り、RELAX NG Compact Syntaxがあります。
ちょっと脱線して別例をあげると、任意のマークアップ処理系を作成可能なDocutilsにはDTDがあります。このDTDがSGML用だったかXML用だったかは失念してしまったのですが、DocutilsにはXML出力もあり、この構造はちゃんと更新されていればDTDと対応するもののはず。

ユースケース エクストリーム編

まだピンと来ないかと思います。文書交換形式としてのXMLが極まると、たとえばJavaプログラムをXSLTでC#に変換し始めたりします。

<transpile from="Java" to="C#" via="XML" with="XSLT"/>
Java形式のプログラムをXML化したりXMLをC#形式にしたりする部分については流されていますが、この部分をInvisible XMLが担当できると、エンドユーザのインターフェイスからは信じて送り出したJavaプログラムが何故かXSLTを通してC#プログラムになって返ってくることになります。Schematronの検証結果も付いてくるかもしれません。

ほかにも非XML文書をXMLとして扱うことができると、XML Pipleline Language(XProc)というXML規格最終合体ロボに非XML文書をシームレスにぶち込めるようになります。

2016年頃まではSteven Pemberton氏が2,3年に一度XML系会議で話している感じですが、2019年からちょっと加速し、2022年に仕様1.0公開となりました。

ユースケース イージー編

  • オレオレ形式の独自マークアップをBNFっぽいルールで記述できれば、それをそのままXML表現にできます(JSONでも良いです。そしてBNFというよりEarleyベース)。XMLにできるなら、XQueryやXPathのセレクタで必要箇所をキャッチしたり、XSLTでEPUB用のXHTMLに変換したり、XProcでzip圧縮してEPUB生成までしたりまでをXMLエコシステムに載っけて作業が可能になります。また、このときにOAXALに則って翻訳システムを活用したりも考えられます。
  • 既存の非XMLbutメイジャーフォーマットのテキストをXML表現にできます。以下略。非XMLのメジャー形式といえばCSVやRTF、PDFなどですね! まあPDFは圧縮かかったり暗号化かかったり他大変なのでixmlはかなり大変でしょうが。

XML構造について

ixmlはテキストからXML構造への対応付けを行うわけですが、となると*ではXML構造とは何か?*について把握しておく必要がありますね。DOMであったりInfoSetであったりPSVIであったりXDMであったりとXMLを扱う構造には色々ありますが、取り敢えず知っておくべきは次のものです;

  • 要素 <elem ....>
  • 属性 <elem attr1="val1" attr2="val2" ..>
  • テキスト ...
  • 処理命令 <? ... ?>
  • コメント <!-- ... -->

「とりあえずXMLに変換する」という段階なので、XML宣言や文書型、XML名前空間などの話は省いています。

要素
: 入れ子可能で、要素やテキストを子に持ちます。属性とは主従の主の関係です。

属性
: 要素と主従の関係で従です。key-valueとして設定し、同じ要素でkeyの重複はありません。HTMLと異なり、valueは必須で、"で囲っている必要があります。

テキスト
: 要素の子です。子は持ちません。

処理命令
: 主にXMLアプリケーションのプロセッサなどのために、XMLアプリケーションの仕様とは別の命令を埋め込むのに利用されます。XML宣言(<?xml ...?>)は処理命令に含まれません。

コメント
: <!--から-->までの間をコメントとします。コメント中では、-->と繋がらない箇所での---のようなハイフンマイナスの連続は使用できません。

XMLの例
<document xml:lang="ja">
	<!-- Comment -->
	<heading level="1">DOCUMENT TITLE HEADING</heading>
	<paragraph>This is a <em>first</em> paragraph.</paragraph>
</document>

上の例では、<document><heading><paragraph><em>が要素です。xml:lang<doc>の属性でjaがその値、level<heading>の属性です。

CoffeePotのインストール・実行

  1. Javaを実行可能にしておく
  2. https://github.com/nineml/coffeepot からファイル一式をダウンロード
  3. ディレクトリに配置
  4. java -jar <coffeepotのパス>/coffeepot-<x.xx.xx>.jar を実行(実行可能かの確認)

実行には処理対象のテキスト、処理する変換ルールを指定する必要があります。

出力は、既定では標準出力へ書き込まれます。出力に失敗した場合はログが表示されます。

コマンドラインインターフェイスの詳細は、引数無指定または--helpで表示されます。動作の詳細については、ninemlのCoffeePotのページを参照するのが良いでしょう。

処理対象テキストの指定

処理対象テキストの指定は、2通りあります;

  • 文字列を直接渡す
  • ファイルのパスを渡す

文字列を直接渡す場合、引数は必要ありません。このとき、単一のspaceがデリミタとして扱われます。

テキストのファイルパスを渡す場合、-iまたは--inputで指定します。

処理対象テキストについてのオプションの代表を紹介します。printは出力或いは表示を意味します(ninemlのCoffeePotの説明を読む限り恐らく)。

-e, --encoding
: 入力の文字エンコーディング。既定はUTF-8
-p, --parse
: パースの指定。既定は1
: パース結果が複数有り得る場合に、どのパース結果をprintするかを指定する。既定が1なので、「ambigousだよ」と言われた後に改めて-p 2のように指定して別の結果を見るために使うのが想定されているのではないか
--parse-count
: パース結果が複数有り得る場合に、printするパース数の指定。既定は1

record

処理対象を単一の文書ではなく、複数の単位のまとまりとして処理したい場合、--recordsを指定します。分割されたものはrecordという単位としてそれぞれixmlルールで処理されます。既定では\nをrecordの分割位置にしますが、--record-startまたは--record-endで正規表現による指定が可能です。

grammarの指定

-g, --grammarでXMLへ変換するためのルールのパスを指定します。ルールの記述については後述します。ルールはixmlを基本としますが、--earleyを指定するとearleyのパーサ、--gllでGLL
のパーサを使用可能です。

ambiguity

処理対象のオプション引数紹介で先行して言及していますが、パース結果が曖昧になることがあります。

「Markdownを処理対象としたのに、CSV用に用意したixmlルールを適用してしまった」とか、「抑、ルールが曖昧」などの理由で、パース結果が一意に定まらないことがあります。その場合、CoffeePotは処理結果のパターンを1つを返したり、複数返したりを指定可能です。

これは開発中の動作確認を意図したものでしょうし、最終的に組み込むものでは曖昧さを残すのは避けた方が良いでしょう。「どこが曖昧であるか」を示すオプション引数--describe-ambiguityを活用すれば、効果的な原因の究明が図れます。

ixmlルール

テキストをXMLに変換していくルールを紹介します。https://coffeepot.nineml.org/ch03.html#earleymapping01 を元にしています。

呼称については参考資料に則していない箇所もあります。

仕様上できないこと

次のことは現在のixml仕様上できません

  • 入力を基に(ルールにない)要素名・属性名を作成すること。 2023-08-31 ちょっと微妙な感じで、函数による加工みたいなことはできないんですが、現在近いことはできるようです
  • 入力"###"に対し"3"を出力するような変換。現状はXSLTやXProcによる変換でこれらを行うことが期待されるようです。 2023-08-31確認 Insertionができるようになっていました。
  • XML名前空間の宣言、名前空間に従う要素・属性の使用。これはCoffeePotではPRAGMAとして実装されています。

https://www.xml.com/articles/2022/03/28/writing-invisible-xml-grammars/ を参照してください。

言及の外になりますが、整形式でないXML、ここでは互い違いのタグ構造を取るようなXMLもできません。ルート無しはともかくタグの対応関係が壊れたものはXMLではないので。

単純ルール

ルールは上から評価されます。
ルールは左辺と右辺に分けられます。左辺をform of a name(本記事では「ルール名」とします)、:を隔てて右辺をsymbolsと呼びます。

document: block* . 

上の例では、documentが左辺、block* .が右辺です。そして、右辺の端の.が、そのルールの終端を意味する終端記号です。

右辺に記述されたsymbolsは、リテラル、別のルール名、区切り、といったもので構成されます。区切り以外のものをsymbolと呼びます。リテラルやルール名には出現についての記号(mark)が付くこともあります。

否定

~をsymbolの先頭に付けることで、そのsymbol以外を指定します。

letter: ~[N]

上の例ではletterルールはN(数字)以外にマッチします。

連続・または

大雑把には、,連続(sequence)、;または(or)を意味する区切りです。

heading: level, title , eol.
@level: lv1; lv2; lv3; lv4; lv5; lv6 , [Zs]+ .
-eol: -#d; -#a; -#0;

heading:level, title,eol .は、headingルールが、levelルール、titleルール、eolルールという並びで構成されることを示しています。

@levelルールは、lv1, [Zs]+またはlv2, [Zs]+または……、ということです。

繰り返し

symbolの繰り返し回数を記号で指定可能です。

*
: 0回以上の繰り返し
+
: 1回以上の繰り返し
?
: 0回または1回

上の記号と使い方はそこそこ馴染みがあると思います。馴染みが無さそうなのが次の2つです;

x**y (x,yはsymbol)
: xの0回以上の繰り返しで、yをデリミタとする
x++y (x,yはsymbol)
: xの1回以上の繰り返しで、yをデリミタとする

//TODO

これらを使えば複数行の段落も上手い具合に指定可能だと思いますが、例示はTODOということで。

グルーピング

()で囲った内部はグループとして扱います。グループとしてまとめた単位で繰り返しやorを指定したいときに有用です。

ul: (-"*", [Zs], li); ul; ol .
li: text -eol .
hr: "--", "-"+ .

リテラル

"で囲われた内部はリテラルです。"###"###にマッチします。

文字セット

[]で囲った内部は、文字セットとして扱われます。["0123456789"]であれば0,1,2,3,4,5,6,7,8,9の何れかであればマッチします。並び順が連続する文字で途中を省略する場合、["A"-"Z"]のように記述します。後述するUnicode指定を使って[#123-#456]のようにも書けます。

,は連続する区間で利用可能です。;でorも利用可能です。

Unicodeまたはカテゴリ

#にHEXでUnicodeを指定した場合は、そのUnicodeとして扱われます。

-eol: -#d?, -#a; -#0;

上の例では改行(CRLF、LF)またはEOLとしてのNULL文字とマッチします。制御コード系とマッチするときに有用です。

ixmlでは、文字セットとして、Unicodeのカテゴリを指定可能です。[Ll]を指定すればLowerCase Letterカテゴリの文字にマッチします。[L]を指定すればLu、Ll、Lm、Lo、Ltといったカテゴリの文字にマッチします。

コメント

コメントは{}で囲います。CoffeePotはPRAGMAの記法にも使っていますが。コメントは出力結果のXMLに含まれません。

document: block* . {コメントです}

要素、属性、テキスト、抑制、既定

print時、既定ではixmlのルール名部分は要素、リテラルの部分がテキストとなります。ルール名に@を前置することで、属性としてprintすることが可能です。また、ルール名・リテラルに-を前置することで、printの抑制が可能です。

document: (id, [Zs])?, [L; Zs;P]* .
@id: -"id=", [N]+ .

上の例は、処理対象の最初の部分にid=1111 という文字列があったとき、1111の部分をid属性の値としてprintします。このときid=という文字列は不要のため、-でprintを抑制します。

id=1111 this is it.

print結果は次になります。

<document id="1111"> this is it.</document>

^をsymbolに前置すると、既定を示します。symbolを辿る終端(リテラルなど)・非終端(ルール名)にあるかで挙動が異なります。

非終端では、そのsymbolが必ず含まれることを意味します。

a: ^nonterminal, fuga. 
nonterminal: hoge .

終端では、symbolがprintに必ず出力されます。

PRAGMA

ixmlの仕様上できないけれど、こんな操作がしたい、可能です。PRAGMAなら。

By pragma we mean, in general, a construct in a formal language which conveys non-standard or out-of-band information to processing software in a way not defined by the specification of the language in which the pragma is embedded. (https://balisage.net/Proceedings/vol27/html/Sperberg-McQueen01/BalisageVol27-Sperberg-McQueen01.html)

ということで、XMLでいうところのProcessing Instructionですね! より一般には拡張用の仕様とでも言うのでしょうか。

PRAGMAの記法は、現在はInvisible XMLの仕様ではありませんが、CoffeePotには実装されています。

PRAGMAの記法(案)

PRAGMAの開始は{[、終了は]}です。コメントの開始{・終了}を外側に持つため、非対応の処理系では無視されることになります。

PRAGMAは3種類あり、これらは記述位置が異なります。

全体レベル(entire grammar)
: 記述位置は最初のルールより前の行。最初のルールまたはルールレベルのPRAGMAの前に1行空行を空ける。
ルールレベル
: 記述位置はルールより前の行。
: 全体レベルとルールレベルのPRAGMAが被ることは無さそうだけれど、カスケードするらしいので、競合する場合はルールレベルが優先するのではないかな多分
: symbolレベル
: 記述位置はsymbolの前。

 {[pragma 全体レベル]} 

{[pragma ルールレベル]}
	rule: {[pragma symbolレベル]}  symbol, symbol.

CoffeePotでのPRAGMA使用の有効・無効

既定では有効です。コマンドラインでは--petandicを指定するとPRAGMAが無視されます。

CoffeePotのPRAGMA

https://coffeepot.nineml.org/ch08.html

PRAGMAの名前空間(というかライブラリ名?)を宣言します。ixmlファイルの最初に書くようにしてください。

{[+pragma nineml "https://nineml.org/ns/pragma/"]}
document: ...

全体レベルPRAGMA

  • csv-columns
  • import
  • XML名前空間
  • record-start, record-end

ルールレベルPRAGMA

  • csv-heading
  • discard-empty
  • combine
  • regex(正規表現)

symbolレベルPRAGMA

  • rename
  • rewrite

renameはsymbol名の書き換え、rewriteはパースしたテキストの書き換えが可能です。

たとえば、xml:langxml:spaceといった、名前に:を持つ属性はsymbol名に指定すると、通常ixmlの方で不正になります。(symbol名にはUnicode指定のエスケープが働かないのかバグなのかは未調査です。)これを暫定的に対処するため次のように記述します。

document: {[nineml rename xml:lang ]}xmllang, text.

資料

脚注
  1. 最近ウォッチし始めたhidarumaからすると大体Norman Walsh氏が動いた結果にみえますが、どうなんでしょう。 ↩︎

  2. しょうもない補足ですが、歌手の遠藤正明氏が十年以上「アニソン界の若獅子」と呼ばれたことにひっかけています。 ↩︎

Discussion