本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。
TEI(Text Encoding Initiative)でテキストを構造化していると、RELAX NG では表現できない種類の規則が出てきます。「本文中の人名には必ず典拠 ID を付ける」「@ref="#foo" の参照先がヘッダに存在すること」「@from は @to より後であってはならない」といった規則です。
RELAX NG は決して非力ではなく、属性の値によって内容モデルを切り替える(co-occurrence constraint)くらいまでは書けますし、DTD Compatibility の ID / IDREF による参照検査も持っています(jing は既定で有効)。ただし「値の大小を比べる」「参照先が <person> 要素であることまで確かめる」「祖先の文脈によって規則を出し分ける」となると手が出ません。
こうした規則は Schematron で書きます。そして TEI では、Schematron を独立したファイルに置くのではなく、スキーマの設計図である ODD(One Document Does it All)の中に埋め込めます。ODD から RELAX NG と Schematron の両方を生成すれば、スキーマの正本が 1 ファイルに収まります。
この記事では、oXygen XML Editor を使わず、macOS + VS Code + コマンドラインだけでその流れを一通り動かします。動作を確認したサンプル一式を組み立てながら、途中で踏んだ落とし穴も記録しています。
サンプルは GitHub に置いてあります。brew install 3 つとスクリプト 2 本で同じ状態を再現できます。
全体の流れ
flowchart LR
A["letters.odd<br/>(カスタマイズ ODD)"] -->|odd2odd.xsl| B["compiled.odd<br/>(P5 と合成)"]
B -->|odd2relax.xsl| C["letters.rng<br/>(RELAX NG)"]
B -->|extract-isosch.xsl| D["letters.sch<br/>(ISO Schematron)"]
D -->|SchXslt2 transpile.xsl| E["letters.sch.xsl<br/>(XSLT 3.0)"]
C -->|jing| F["構造の検証"]
E -->|Saxon| G["SVRL(検証レポート)<br/>→ 規則の検証"]
ODD をそのまま変換するのではなく、いったん TEI P5 の全仕様と合成した「展開済み ODD」を作るのがポイントです。カスタマイズ ODD には「core モジュールを使う」としか書いていないので、実際の要素定義を P5 から引き込む工程が要ります。
環境構築
必要なものは Java、Saxon、jing の 3 つです。
brew install openjdk saxon jing-trang
Homebrew の openjdk は /Library/Java/JavaVirtualMachines/ にシンボリックリンクを張らない構成のことがあり、その場合 java が PATH に乗りません。以下で確認できます。
java -version # 見つからないと言われることがある
/opt/homebrew/opt/openjdk/bin/java -version # こちらは動く
見つからない場合は、スクリプト側で PATH を通す方が副作用が少なく済みます(後述のスクリプトはそうしています)。
次の 3 つはダウンロードして展開します。
| 名前 | 役割 | 入手元 |
|---|---|---|
| TEI Stylesheets | ODD を RELAX NG / Schematron に変換する XSLT 群 | TEIC/Stylesheets のリリース |
| SchXslt2 | Schematron を XSLT 3.0 に変換するトランスパイラ | SchXslt/schxslt2(Codeberg) |
| p5subset.xml | TEI P5 の全仕様を 1 ファイルにまとめたもの | TEI Vault |
scripts/setup-tools.zsh としてまとめておきます。
#!/usr/bin/env zsh
set -euo pipefail
ROOT=${0:A:h:h}
cd "$ROOT"
TEI_XSL_VERSION=7.61.0
SCHXSLT_VERSION=1.11.2
mkdir -p tools
cd tools
if [[ ! -d tei-xsl ]]; then
curl -fsSL -o tei-xsl.zip "https://github.com/TEIC/Stylesheets/releases/download/v${TEI_XSL_VERSION}/tei-xsl-${TEI_XSL_VERSION}.zip"
unzip -q -o tei-xsl.zip -d tei-xsl
rm tei-xsl.zip
fi
if [[ ! -d schxslt2 ]]; then
curl -fsSL -o schxslt2.zip "https://codeberg.org/SchXslt/schxslt2/releases/download/v${SCHXSLT_VERSION}/schxslt2-${SCHXSLT_VERSION}.zip"
unzip -q -o schxslt2.zip -d schxslt2
rm schxslt2.zip
fi
[[ -f p5subset.xml ]] || curl -fsSL -o p5subset.xml "https://www.tei-c.org/Vault/P5/current/xml/tei/odd/p5subset.xml"
SchXslt2 の配布物は transpile.xsl を含むごく小さな zip(18 KB)です。TEI Stylesheets は 12 MB、p5subset.xml は 4.1 MB でした。
ODD に制約を書く
書簡コーパス用の小さなカスタマイゼーションを作ります。全体は次のような構造です。
<schemaSpec ident="letters" start="TEI" prefix="tei_" docLang="ja" targetLang="ja">
<constraintDecl scheme="schematron" queryBinding="xslt2"/>
<moduleRef key="tei"/>
<moduleRef key="core"/>
<moduleRef key="header"/>
<moduleRef key="textstructure"/>
<moduleRef key="namesdates"/>
<moduleRef key="corpus"/>
<!-- ここに constraintSpec を並べる -->
</schemaSpec>
<constraintDecl> で「Schematron を使うこと」と「XPath のバージョン」を宣言します。queryBinding="xslt2" にしておくと matches() や xs:date() のような XPath 2.0 以降の関数が使えます。指定しない場合も TEI Stylesheets が xslt2 を補います(extract-isosch.xsl に ( //tei:constraintDecl/@queryBinding, 'xslt2')[1] というハードコードがあります)。ただし ISO Schematron 本体の既定値は xslt(XPath 1.0)なので、生成した .sch を別の処理系に渡すことも考えると明示しておく方が安全です。TEI Guidelines 上も @queryBinding は「推奨(rec)」扱いです。
要素固有の制約は elementSpec の中に置く
<persName> に対する 2 つの規則です。
<elementSpec ident="persName" module="namesdates" mode="change">
<constraintSpec ident="persName-needs-ref" scheme="schematron" mode="add">
<desc>本文中の <persName> には @ref が必要。</desc>
<constraint>
<sch:rule context="tei:body//tei:persName">
<sch:assert test="@ref"
>本文中の <persName> には @ref(人物典拠への参照)が必要です。対象:
「<sch:value-of select="normalize-space(.)"/>」</sch:assert>
</sch:rule>
</constraint>
</constraintSpec>
<constraintSpec ident="persName-ref-resolves" scheme="schematron" mode="add">
<desc>@ref="#foo" は同一文書内の <person xml:id="foo"> を指していること。</desc>
<constraint>
<sch:rule context="tei:persName[starts-with(@ref, '#')]">
<sch:let name="target" value="substring-after(@ref, '#')"/>
<sch:assert test="//tei:person[@xml:id eq $target]"
>@ref の参照先が見つかりません: <sch:value-of select="@ref"/></sch:assert>
</sch:rule>
</constraint>
</constraintSpec>
</elementSpec>
1 つめは、RELAX NG でも @ref を必須にはできますが、「本文中のものだけ」という条件は付けられません。ヘッダの <person><persName>森鴎外</persName></person> には @ref を要求したくないので、context を tei:body//tei:persName に絞っています。
2 つめは参照整合性です。RELAX NG の ID / IDREF でも「その値を持つ xml:id がどこかにある」ことまでは言えますが、「参照先が <person> 要素であること」まで縛ることはできません。ここは Schematron の領分です。<sch:let> で変数を作り、<sch:value-of> でエラーメッセージに実際の値を埋め込んでいます。
なお、Schematron の変数要素は <sch:let> です。<sch:variable> ではありません(XSLT の xsl:variable と混同しやすい箇所です)。
assert と report の使い分け
<assert> は「この条件が真であるべき」、<report> は「この条件が真なら困る」という向きです。同じことを書けますが、否定を重ねずに済む方を選ぶと読みやすくなります。
<elementSpec ident="pb" module="core" mode="change">
<constraintSpec ident="pb-not-first-in-div" scheme="schematron" mode="add">
<desc><pb> を <div> の先頭に置かない。</desc>
<constraint>
<sch:rule context="tei:pb">
<sch:report test="parent::tei:div and not(preceding-sibling::*)"
><pb> が <div> の先頭にあります。<div> の直前に移動してください。
(n=<sch:value-of select="@n"/>)</sch:report>
</sch:rule>
</constraint>
</constraintSpec>
</elementSpec>
文書全体にかかる制約は schemaSpec 直下に置く
特定の要素の定義に属さない規則は、<schemaSpec> の直下に <constraintSpec> を置きます。
<constraintSpec ident="isbn-format" scheme="schematron">
<desc><idno type="ISBN"> はハイフン入り 13 桁 ISBN であること。</desc>
<constraint>
<sch:rule context="tei:idno[@type eq 'ISBN']">
<sch:assert test="matches(normalize-space(.), '^97[89]-\d{1,5}-\d{1,7}-\d{1,7}-\d$')"
>ISBN の書式が不正です: 「<sch:value-of select="normalize-space(.)"/>」
期待する形式: 978-4-12-345678-9</sch:assert>
</sch:rule>
</constraint>
</constraintSpec>
<constraintSpec ident="date-range-order" scheme="schematron">
<desc>@from は @to より後であってはならない。</desc>
<constraint>
<sch:rule context="tei:date[@from][@to]">
<sch:assert test="xs:date(@from) le xs:date(@to)"
>日付の前後が逆です: @from=<sch:value-of select="@from"/> /
@to=<sch:value-of select="@to"/></sch:assert>
</sch:rule>
</constraint>
</constraintSpec>
matches() による正規表現と、xs:date() による日付比較です。どちらも queryBinding="xslt2" があって初めて使えます。
スキーマを生成する
4 ステップです。
export PATH="/opt/homebrew/opt/openjdk/bin:$PATH" # java が PATH に無い場合
XSL=tools/tei-xsl/xml/tei/stylesheet/odds
# 1. カスタマイズ ODD を P5 と合成して展開済み ODD を作る
saxon -s:odd/letters.odd -xsl:$XSL/odd2odd.xsl \
-o:build/letters.compiled.odd defaultSource="$PWD/tools/p5subset.xml"
# 2. RELAX NG を生成
saxon -s:build/letters.compiled.odd -xsl:$XSL/odd2relax.xsl -o:build/letters.rng
# 3. ISO Schematron を抽出
saxon -s:build/letters.compiled.odd -xsl:$XSL/extract-isosch.xsl -o:build/letters.sch
# 4. Schematron を実行可能な XSLT 3.0 に変換
saxon -s:build/letters.sch -xsl:tools/schxslt2/schxslt2-1.11.2/transpile.xsl \
-o:build/letters.sch.xsl
(zsh に貼り付けるときは行継続の \ が落ちて引数が次行に渡らない事故があるので、実際に使うときは \ を外して 1 コマンド 1 行にしてください。)
defaultSource を指定しないと、変換のたびに tei-c.org から p5subset.xml を取りに行きます。ローカルに置いた方が速く、オフラインでも動きます。
手元で生成された成果物のサイズは次のとおりでした。
letters.compiled.odd 2.1M
letters.rng 494K (10,968 行)
letters.sch 13K
letters.sch.xsl 100K
生成された letters.sch には 36 個の <pattern> が入っていました。自分で書いたのは 5 個だけで、残る 31 個は TEI P5 が Guidelines 自体に持っている制約です。
実際の id には schematron-constraint- という接頭辞が付きます。以下はそれを除いた部分です。
CMC_generatedBy_within_post-1
att-datable-w3c-when-2
att-datable-w3c-from-3
att-datable-w3c-to-4
only_1_ODD_source-5
targetLang-6
spanTo-points-to-following-7
...
「@when は他の日付属性と併用できない」「@spanTo は後方を指していること」といった規則です。RELAX NG だけで検証していると、これらは一切チェックされません。ODD から Schematron を生成する価値は、自分で書いた規則よりむしろこちら側にあるかもしれません。
検証する
RELAX NG は jing、Schematron は生成した XSLT を Saxon で実行します。
jing build/letters.rng data/invalid.xml
saxon -s:data/invalid.xml -xsl:build/letters.sch.xsl -o:report.svrl
Schematron の出力は SVRL(Schematron Validation Report Language)という XML です。違反は 2 種類の要素で表現されます。
svrl:failed-assert…<assert>の test が false だった箇所svrl:successful-report…<report>の test が true だった箇所
名前は対照的ですが、どちらも「直すべき箇所」です。
SVRL は違反箇所を XPath で示します。エディタが必要とするのは行番号なので、変換する必要があります。当初は XSLT(XSL Transformations)で書こうとしましたが、saxon:line-number() は Saxon-HE(Home Edition)では使えませんでした。
XPST0017 Cannot find a 1-argument function named Q{http://saxon.sf.net/}line-number().
Saxon extension functions are not available under Saxon-HE
Saxon の拡張関数は PE(Professional Edition)以上の機能です。代わりに、検証対象の XML を Python の expat で舐めて「要素パス → 行番号」の対応表を作り、SVRL の @location と突き合わせることにしました。標準ライブラリだけで済みます。
import xml.parsers.expat
def build_line_map(path):
"""要素パス (uri, local, index) のタプル列 → (行, 桁) の辞書を作る。"""
line_map, stack, counters = {}, [], [{}]
def start(name, _attrs):
uri, _, local = name.rpartition("|") if "|" in name else ("", "", name)
counters[-1][(uri, local)] = counters[-1].get((uri, local), 0) + 1
stack.append((uri, local, counters[-1][(uri, local)]))
counters.append({})
line_map[tuple(stack)] = (parser.CurrentLineNumber,
parser.CurrentColumnNumber + 1)
def end(_name):
counters.pop()
stack.pop()
parser = xml.parsers.expat.ParserCreate(namespace_separator="|")
parser.StartElementHandler = start
parser.EndElementHandler = end
with open(path, "rb") as fh:
parser.ParseFile(fh)
return line_map
SVRL の @location は EQName(expanded QName、名前空間 URI を展開した修飾名)形式(/Q{http://www.tei-c.org/ns/1.0}TEI[1]/...)なので、正規表現で分解して同じ形のタプル列にすれば照合できます。
出力形式は jing に合わせて 絶対パス:行:桁: severity: メッセージ にしました。これで RELAX NG と Schematron の結果を同じ問題マッチャで拾えます。
検証結果
RELAX NG では妥当だが Schematron では 5 か所違反する、というサンプルを用意して実行しました。
── data/invalid.xml ──────────────────────────────────────
✅ RELAX NG: 構造は妥当
/path/to/data/invalid.xml:23:11: error: ISBN の書式が不正です: 「4-12-345678-X」 期待する形式: 978-4-12-345678-9 [schematron-constraint-isbn-format-35]
/path/to/data/invalid.xml:30:9: error: 日付の前後が逆です: @from=1911-12-31 / @to=1911-01-01 [schematron-constraint-date-range-order-36]
/path/to/data/invalid.xml:45:9: error: <pb> が <div> の先頭にあります。<div> の直前に移動してください。 (n=1) [schematron-constraint-pb-not-first-in-div-20]
/path/to/data/invalid.xml:50:15: error: 本文中の <persName> には @ref(人物典拠への参照)が必要です。対象: 「正岡子規」 [schematron-constraint-persName-needs-ref-30]
/path/to/data/invalid.xml:53:12: error: @ref の参照先が見つかりません: #soseki (<particDesc> に xml:id="soseki" の <person> を追加してください) [schematron-constraint-persName-ref-resolves-31]
❌ Schematron 違反(上記)
── data/valid.xml ──────────────────────────────────────
✅ RELAX NG: 構造は妥当
✅ Schematron: 制約もすべて満たしている
RELAX NG は両方のファイルを「妥当」と判定しています。5 つの違反はいずれも構造としては正しく、値や参照関係だけが誤っているためです。この差が Schematron を併用する理由そのものです。
VS Code に組み込む
RELAX NG はインラインで検証される
Red Hat の XML 拡張(redhat.vscode-xml、内部は LemMinX)が RELAX NG に対応しています。公式ドキュメント上は experimental 扱い(v0.22.0 以降)ですが、TEI の RNG では問題なく動きました。
code --install-extension redhat.vscode-xml
TEI 文書の冒頭に <?xml-model?> 処理命令を書いておけば、拡張がそれを解決して構造エラーを波線で表示します。
<?xml-model href="../build/letters.rng" type="application/xml"
schematypens="http://relaxng.org/ns/structure/1.0"?>
相対パスも解決されます。実際に効いているかを確かめるため、拡張が同梱している言語サーバ(LemMinX)を直接起動し、Language Server Protocol(LSP)の textDocument/didOpen を送って診断を受け取ってみました。エディタを開かずに「拡張が何を検証しているか」を確認できます。
<p> の中に <opener> を入れて構造を壊したファイルを渡すと、次のようになります。
$ ./scripts/lsp-probe.py probe/broken-structure.xml
diagnostics: 1 件
L48:17 [xml/out_of_context_element] element "opener" not allowed here; expected
the element end-tag, text or element "abbr", "add", "addName", "ad...
行番号・列番号とも正しく、生成した letters.rng が参照されていることが確認できました。
Schematron はタスクで回す
この拡張は Schematron には対応していません。対応要望は issue #451 として 2021 年に立てられて以来 open のままで、<?xml-model schematypens="http://purl.oclc.org/dsdl/schematron"?> を書いても無視されます。同じ言語サーバに、Schematron 制約に 5 か所違反するファイル(RELAX NG としては妥当)を渡すと、診断は 0 件でした。
$ ./scripts/lsp-probe.py data/invalid.xml
diagnostics: 0 件
Schematron の <?xml-model?> は oXygen XML Editor では効くので、書いておくこと自体に意味はあります。VS Code では別経路が必要、ということです。
代わりに、検証スクリプトを VS Code のタスクとして登録し、problemMatcher で結果を Problems パネルに流します。
{
"version": "2.0.0",
"tasks": [
{
"label": "TEI: スキーマ生成 (ODD → RNG + SCH)",
"type": "shell",
"command": "./scripts/build-schema.zsh",
"group": "build",
"problemMatcher": []
},
{
"label": "TEI: 開いているファイルを検証",
"type": "shell",
"command": "./scripts/validate.zsh '${relativeFile}'",
"group": { "kind": "test", "isDefault": true },
"problemMatcher": {
"owner": "tei",
"source": "jing / SchXslt2",
"fileLocation": "absolute",
"pattern": {
"regexp": "^(/.+?):(\\d+):(\\d+):\\s+(error|warning|fatal):\\s+(.*)$",
"file": 1, "line": 2, "column": 3, "severity": 4, "message": 5
}
}
}
]
}
jing の出力も 絶対パス:行:桁: error: メッセージ 形式なので、この 1 つの正規表現で RELAX NG と Schematron の両方の違反を拾えます。Problems パネルの行をクリックすれば該当箇所にジャンプします。
.vscode/settings.json では、ODD ファイルの扱いを決めておきます。
{
"files.associations": { "*.odd": "xml" },
"xml.fileAssociations": [
{
"pattern": "odd/**.odd",
"systemId": "https://www.tei-c.org/release/xml/tei/custom/schema/relaxng/tei_odds.rng"
}
],
"files.exclude": { "build": true, "tools": true }
}
拡張子 .odd は既定では XML と認識されないので明示します。ODD 自身も TEI 文書なので、tei_odds.rng を当てておくと ODD を書いている最中に構造エラーが出ます。
踏んだ落とし穴
<constraint> の直下に <assert> を書くと黙って消える
これが一番わかりにくい挙動でした。<sch:rule> を省略して <constraint> の直下に <sch:assert> を置くと、生成された .sch からその制約が消えます。
WARNING: Ignoring invalid sch:assert found directly within <constraint>.
警告は標準エラー出力に出るだけで、変換自体は成功します。終了コードも 0 です。生成された .sch を grep すると該当パターンは 0 件でした。
grep -c "persName-needs-ref" build/letters.sch
# → 0
<constraint> の中には必ず <sch:rule context="..."> を書き、context を自分で指定する必要があります(<sch:pattern> は省略でき、TEI Stylesheets が自動で補います)。ビルドスクリプトの出力を捨てずに眺めておく方が安全です。
これは Stylesheets の気まぐれではなく TEI の仕様です。TEI P5 の <constraintSpec> 自身が context-required という Schematron 制約を持っていて、「<sch:assert> / <sch:report> は @context を持つ <sch:rule> の子孫でなければならない」と定めています。extract-isosch.xsl はそれを前提に、条件を満たさないものを警告つきで捨てているだけです。
やっかいなのは、<constraint> の内容モデルが (text | anyElement)* なので、ODD を RELAX NG で検証しても何も言われない点です。この規則自体は tei_odds.rng に Schematron として埋め込まれているのですが、後述の LemMinX は Schematron を実行しないため、VS Code 上では検知できません。ODD 自身の Schematron まで効かせたければ、oXygen を使うか、tei_odds.rng から Schematron を抽出して ODD にも同じパイプラインを通すことになります。
jing 内蔵の Schematron は使えない
jing は ISO Schematron のスキーマリーダを内蔵していますが(例外のスタックトレースに com.thaiopensource.validate.schematron.ISOSchemaReaderImpl が出てきます)、.sch を直接渡すと Saxon の初期化で落ちました。
Exception in thread "main" java.lang.IllegalArgumentException: Unknown XPath version 0
at net.sf.saxon.Configuration.newExpressionParser(Configuration.java:2804)
at com.thaiopensource.validate.schematron.ISOSchemaReaderImpl.createSchema(...)
最初は queryBinding="xslt2" が原因かと思いましたが、queryBinding を書かない最小の Schematron でも同じ例外が出るので、これは違いました。
== queryBinding なし
Exception in thread "main" java.lang.IllegalArgumentException: Unknown XPath version 0
== queryBinding="xslt2"
Exception in thread "main" java.lang.IllegalArgumentException: Unknown XPath version 0
jing 20241231 が同梱している Saxon は 9.3.0.4(jing-20241231/bin/saxon9.jar)で、この組み合わせでは ISO Schematron のリーダがそもそも動かないようです。付属ドキュメント(doc/jing.html)も「Schematron 1.5 に対応」という記述のまま止まっています。jing は RELAX NG 専用と割り切り、Schematron は SchXslt2 + Saxon で回すのが確実でした。
<particDesc> は corpus モジュール
人物一覧をヘッダに置こうとして、namesdates モジュールだけを取り込んだところ次のエラーになりました。
error: element "particDesc" not allowed anywhere; expected the element end-tag or
element "abstract", "calendarDesc", "correspDesc", "creation", "langUsage" or "textClass"
<listPerson> や <person> は namesdates ですが、それらを収める <particDesc> は corpus モジュールにあります。<moduleRef key="corpus"/> を足して解決しました。モジュールを絞ったカスタマイゼーションでは、要素そのものと「置き場所」が別モジュールに分かれている場合があります。
Saxon-HE には拡張関数がない
前述のとおり saxon:line-number() は使えません。SVRL から行番号を得たい場合は、Saxon-PE 以上を使うか、別の手段で解決する必要があります。SVRL 自体は XPath ロケーションを持っているので、情報が失われているわけではありません。
手元の構成
最終的なディレクトリ構成です。
schematron-odd/
├── .gitignore build/ と tools/ を除外
├── README.md セットアップ手順と注意点
├── .vscode/
│ ├── extensions.json redhat.vscode-xml を推奨
│ ├── settings.json .odd の関連付け・ODD 自身の検証
│ └── tasks.json 生成・検証タスク + problemMatcher
├── odd/
│ └── letters.odd スキーマの正本(Schematron 込み)
├── data/
│ ├── valid.xml すべての規則を満たすサンプル
│ └── invalid.xml RELAX NG は通るが Schematron で 5 か所違反
├── probe/
│ └── broken-structure.xml 構造を壊したファイル(LSP 診断の確認用)
├── scripts/
│ ├── setup-tools.zsh ツール取得(初回のみ)
│ ├── build-schema.zsh ODD → RNG + SCH + SCH.XSL
│ ├── validate.zsh jing + Saxon で検証
│ ├── svrl-report.py SVRL → ファイル:行:桁 形式
│ └── lsp-probe.py LemMinX に直接 LSP を喋って診断を確認
├── build/ 生成物(.gitignore)
└── tools/ ダウンロードしたツール(.gitignore)
build/ と tools/ は .gitignore に入れています。setup-tools.zsh と build-schema.zsh で再現できるので、リポジトリに 2.1 MB の展開済み ODD を置く必要はありません(リポジトリは 16 ファイル・約 1,100 行に収まっています)。
validate.zsh は違反が 1 件でもあれば終了コード 1 を返すので、そのまま CI に載せられます。
動作を確認した環境
| 項目 | バージョン |
|---|---|
| macOS | Darwin 25.5.0 (Apple Silicon) |
| OpenJDK | 25.0.2 (Homebrew) |
| Saxon-HE | 12.9 (Homebrew saxon) |
| jing | 20241231 (Homebrew jing-trang) |
| TEI Stylesheets | 7.61.0 |
| SchXslt2 | 1.11.2 |
| VS Code | 1.129.1 |
| redhat.vscode-xml | 0.29.3 (LemMinX 0.31.2) |
oXygen XML Editor があれば ODD の編集から RELAX NG / Schematron の生成・検証まで GUI で完結しますが、コマンドラインで組んでおくと CI に載せられること、どの工程で何が起きているかが見えることが利点になります。ODD に書いた <constraint> が最終的にどんな XSLT になるかまで追えるので、規則が意図どおりに効かないときの原因も絞りやすくなります。



コメント
…