:::message
人手で検証を行った後、AIが記事を執筆しました。
:::
はじめに
TEI(Text Encoding Initiative)XMLを編集する際、要素や属性の構造検証だけでなく、より複雑なビジネスルールの検証が必要になることがあります。本記事では、RELAX NG(RNG)とSchematronを組み合わせて、構造検証と内容検証の両方を実現する方法を、実際のプロジェクトで直面した課題を例に解説します。
解決したい課題
日本の古典文学テキストをTEI XMLで校訂する際、以下のような要求がありました:
-
ID参照の動的検証:
corresp
属性で参照するIDが、実際に文書内のwitness
要素に存在することを検証したい - Oxygen XML Editorでの補完機能: 編集時にIDの候補を自動表示したい
- 複数ID参照のサポート: スペース区切りで複数のIDを指定可能にしたい
-
特定要素のみ参照を許可:
witness
要素のIDのみを参照可能とし、person
要素のIDが含まれる場合はエラーにしたい
なぜRNG + Schematronなのか?
RELAX NGの得意分野
- 要素・属性の構造定義
- データ型の指定
- 基本的な内容モデルの定義
Schematronの得意分野
- XPathベースの複雑な検証ルール
- 文書内の相互参照チェック
- カスタムエラーメッセージの提供
この2つを組み合わせることで、構造と内容の両面から厳密な検証が可能になります。
実装例
1. 基本的なRNGスキーマ構造
<?xml version="1.0" encoding="UTF-8"?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0"
xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0"
xmlns:sch="http://purl.oclc.org/dsdl/schematron"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"
ns="http://www.tei-c.org/ns/1.0">
<!-- Schematron名前空間宣言 -->
<sch:ns prefix="tei" uri="http://www.tei-c.org/ns/1.0"/>
<!-- ここにSchematronルールを埋め込む -->
<start>
<ref name="TEI"/>
</start>
<!-- RNGによる構造定義 -->
</grammar>
2. ID定義とanyURI型の活用
Oxygen XML Editorで自動補完を実現するために、anyURI
型を使用します:
<!-- 証本リスト -->
<define name="listWit">
<element name="listWit">
<oneOrMore>
<element name="witness">
<attribute name="xml:id">
<data type="ID"/>
</attribute>
<text/>
</element>
</oneOrMore>
</element>
</define>
<!-- 底本読み -->
<define name="lem">
<element name="lem">
<attribute name="corresp">
<a:documentation>
証本への参照
#付きのIDREF形式で内部参照
Oxygenでは#付きのxml:id一覧が表示されます
</a:documentation>
<list>
<oneOrMore>
<data type="anyURI"/>
</oneOrMore>
</list>
</attribute>
<text/>
</element>
</define>
ポイント:
-
data type="ID"
で一意性を保証 -
data type="anyURI"
で#
付きの内部参照を許可 -
list
要素でスペース区切りの複数値を許可
3. Schematronによる高度な検証
<sch:pattern id="witness-references">
<sch:title>Witness ID参照の検証</sch:title>
<sch:rule context="tei:lem[@corresp]">
<sch:let name="listWitIds" value="//tei:listWit/tei:witness/@xml:id"/>
<sch:let name="listPersonIds" value="//tei:listPerson/tei:person/@xml:id"/>
<sch:let name="correspTokens" value="tokenize(normalize-space(@corresp), 's+')"/>
<!-- witnessのみを参照すべき -->
<sch:assert test="every $token in $correspTokens
satisfies (
starts-with($token, '#') and
substring($token, 2) = $listWitIds
)" role="error">
corresp属性は証本(witness)のIDのみを参照すべきです。
利用可能なwitness ID: #<sch:value-of select="string-join($listWitIds, ', #')"/>
</sch:assert>
<!-- person IDが含まれている場合のエラー -->
<sch:report test="some $token in $correspTokens
satisfies (
starts-with($token, '#') and
substring($token, 2) = $listPersonIds
)" role="error">
corresp属性に人物(person)のIDが含まれています。
検出されたperson ID: <sch:value-of select="
string-join(
for $token in $correspTokens
return if (starts-with($token, '#') and substring($token, 2) = $listPersonIds)
then $token
else (),
', '
)
"/>
</sch:report>
</sch:rule>
</sch:pattern>
ポイント:
-
sch:let
で変数を定義し、XPathで動的に値を取得 -
tokenize()
で複数ID参照をパース -
sch:assert
で条件を満たさない場合にエラー -
sch:report
で条件を満たす場合にエラー -
role="error"
でエラーレベルを指定(warning、info も可能)
4. 実際の使用例
<!-- XML文書での使用 -->
<?xml-model href="schema.rng" type="application/xml"
schematypens="http://relaxng.org/ns/structure/1.0"?>
<?xml-model href="schema.rng" type="application/xml"
schematypens="http://purl.oclc.org/dsdl/schematron"?>
<TEI xmlns="http://www.tei-c.org/ns/1.0">
<teiHeader>
<listWit>
<witness xml:id="aaa">証本A</witness>
<witness xml:id="iii">証本I</witness>
</listWit>
<listPerson>
<person xml:id="abc">
<persName>人物ABC</persName>
</person>
</listPerson>
</teiHeader>
<text>
<body>
<app>
<!-- 正しい例:witnessのみ参照 -->
<lem corresp="#aaa #iii">本文</lem>
<rdg corresp="#aaa">別の読み</rdg>
</app>
<app>
<!-- エラー例:personを含む -->
<lem corresp="#aaa #abc">本文</lem>
<rdg>別の読み</rdg>
</app>
</body>
</text>
</TEI>
実装時の注意点
1. XPath 2.0の構文
Schematron内のXPath式では、for式の構文に注意が必要です:
<!-- 正しい -->
let $invalid := (
for $token in $correspTokens
return
let $id := substring($token, 2)
return if ($id = $validIds) then () else $token
)
<!-- エラーになる -->
let $invalid := for $token in $correspTokens
let $id := substring($token, 2)
return if ($id = $validIds) then () else $token
2. IDREF vs anyURI
-
IDREF型:
#
を含めることができず、Oxygenでの補完が制限される -
anyURI型:
#
付きの値を許可し、Oxygenが自動的にID補完を提供
3. Schematronのrole属性
-
role="error"
: 赤色のエラーマーカー -
role="warning"
: 黄色の警告マーカー -
role="info"
: 青色の情報マーカー
応用例
複雑な相互参照の検証
<sch:pattern id="cross-references">
<!-- app要素には必ずlem要素が1つ必要 -->
<sch:rule context="tei:app">
<sch:assert test="count(tei:lem) = 1">
app要素には必ず1つのlem要素が必要です
</sch:assert>
</sch:rule>
<!-- rdg要素のcorrespはlem要素と重複不可 -->
<sch:rule context="tei:rdg[@corresp]">
<sch:let name="lemCorresp" value="../tei:lem/@corresp"/>
<sch:assert test="not(@corresp = $lemCorresp)">
rdg要素のcorrespはlem要素と異なる値にしてください
</sch:assert>
</sch:rule>
</sch:pattern>
条件付き必須属性
<sch:pattern id="conditional-attributes">
<sch:rule context="tei:date[@when]">
<!-- when属性がある場合、ISO形式でなければならない -->
<sch:assert test="matches(@when, '^d{4}-d{2}-d{2}$')">
when属性はYYYY-MM-DD形式で指定してください
</sch:assert>
</sch:rule>
</sch:pattern>
まとめ
RELAX NGとSchematronを組み合わせることで:
- 構造検証と内容検証の分離: それぞれの得意分野を活かした設計が可能
- 動的な検証ルール: 文書の内容に基づいた柔軟な検証
- エディタ支援: Oxygen XML Editorなどでの高度な編集支援
- わかりやすいエラーメッセージ: 日本語でのカスタムメッセージ
特にTEI XMLのような複雑な構造を持つ文書の編集において、この組み合わせは非常に強力なツールとなります。
参考資料
- RELAX NG Compact Syntax Tutorial
- Schematron Quick Reference
- TEI Guidelines
- Oxygen XML Editor Documentation
この記事で紹介したスキーマの完全なコードは、実際のプロジェクトで使用されているものです。同様の課題を抱えている方の参考になれば幸いです。