競プロ備忘録

競プロerの備忘録

XMLのお勉強その3 - XML 1.0 ②

前回

XMLのお勉強その2 - XML 1.0 - 競プロ備忘録

前回はXML 1.0仕様書の2章まで読んだので、3章から読んでいきましょう。

前回注記をつけておくべきだったかも知れませんが、仕様書の焼き増しになるだけなので、基本的に原文の引用は載せません。
ただ、用語の定義や妥当性制約・整形式性制約だけは引用して載せるようにしています。

本編

3 Logical Structures

意外にもここまで定義がありませんでしたが、「要素」の定義です。

[Definition: Each XML document contains one or more elements, the boundaries of which are either delimited by start-tags and end-tags, or, for empty elements, by an empty-element tag. Each element has a type, identified by name, sometimes called its "generic identifier" (GI), and may have a set of attribute specifications.]

XMLを一度も見たことがないという人もあまりいないと思うので、まあ想像したとおりのものです。
ちょっと聞き慣れなかったのが要素の型の話で、名前で区別される、と述べられています。また、その名前のことを「共通識別子」(Generic Identifier, GI)と呼ぶとも述べられています。
要素型、属性の名前について、仕様は構文の域を超えて制約を課しませんが、('X'|'x')('M'|'m')('L'|'l')にマッチする文字列から始まる名前は将来用途に予約されています。

続いて、要素の整形式性制約・妥当性制約が述べられます。

Well-formedness constraint: Element Type Match
The Name in an element's end-tag MUST match the element type in the start-tag.

Validity constraint: Element Valid
An element is valid if there is a declaration matching elementdecl where the Name matches the element type, and one of the following holds:

  1. The declaration matches EMPTY and the element has no content (not even entity references, comments, PIs or white space).
  2. The declaration matches children and the sequence of child elements belongs to the language generated by the regular expression in the content model, with optional white space, comments and PIs (i.e. markup matching production [27] Misc) between the start-tag and the first child element, between child elements, or between the last child element and the end-tag. Note that a CDATA section containing only white space or a reference to an entity whose replacement text is character references expanding to white space do not match the nonterminal S, and hence cannot appear in these positions; however, a reference to an internal entity with a literal value consisting of character references expanding to white space does match S, since its replacement text is the white space resulting from expansion of the character references.
  3. The declaration matches Mixed, and the content (after replacing any entity references with their replacement text) consists of character data (including CDATA sections), comments, PIs and child elements whose types match names in the content model.
  4. The declaration matches ANY, and the content (after replacing any entity references with their replacement text) consists of character data, CDATA sections, comments, PIs and child elements whose types have been declared.

整形式性制約のほうはかんたんで、開始タグと終了タグの名前は一致しなければいけないという当たり前の制約です。

妥当性制約については、まず対応する名前を持つ要素型宣言がある必要があり、その上で4つのうちいずれかの制約を満たすとき妥当であると言っています。
1つ目は宣言がEMPTYにマッチし何ら内容を持たないときで、「何ら内容を持たない」とは、実体参照、コメント、処理命令、ホワイトスペースなども含めたいかなる内容も持たないことを意味します。
2つ目は宣言がchildren(生成規則[47])にマッチし、子要素が内容モデルの正規表現から生成される言語に属する場合です。子要素の間や前後にはホワイトスペース、コメント、処理命令、またはホワイトスペースに展開される文字参照からなるリテラル値を持つ内部実体への参照を含んでも構いません。これは生成規則[27]にマッチするテキストであって、ホワイトスペースのみを含むCDATAセクションや、ホワイトスペースに展開される文字参照を置換テキストに持つ実体への参照はマッチしないとも述べられています。参照についての記述がエラいややこしいです…これは実体参照の展開について確認した後でないと理解できないのかもしれません。
3つ目は宣言がMixed(生成規則[51])にマッチし、全ての実体参照をそれぞれの置換テキストで置き換えた後の要素内容が文字データ、コメント、処理命令、要素型が内容モデルに含まれる名前にマッチする子要素から成る場合です。
4つ目は宣言がANYにマッチし、全ての実体参照をそれぞれの置換テキストで置き換えた後の要素内容が文字データ、コメント、処理命令、宣言済の要素型を持つ子要素から成る場合です。

3.1 Start-Tags, End-Tags, and Empty-Element Tags

まず「開始タグ」の定義です。

[Definition: The beginning of every non-empty XML element is marked by a start-tag.]

続いて「属性指定」「属性名」「属性値」の定義です。

[Definition: The Name-AttValue pairs are referred to as the attribute specifications of the element]
[Definition: with the Name in each pair referred to as the attribute name ]
[Definition: the content of the AttValue (the text between the ' or " delimiters) as the attribute value.]

いずれも取り立てて注意すべき点はなさそうです。属性指定の順序は有意ではない、という点のみ、仕様で注記がついています。

続いて、属性に関する制約です。

Well-formedness constraint: Unique Att Spec
An attribute name MUST NOT appear more than once in the same start-tag or empty-element tag.
Validity constraint: Attribute Value Type
The attribute MUST have been declared; the value MUST be of the type declared for it. (For attribute types, see 3.3 Attribute-List Declarations.) Well-formedness constraint: No External Entity References
Attribute values MUST NOT contain direct or indirect entity references to external entities.
Well-formedness constraint: No < in Attribute Values
The replacement text of any entity referred to directly or indirectly in an attribute value MUST NOT contain a <.

重複した属性指定、外部実体への参照を含む属性値、小なりを含む属性値が整形式制約違反となります。
また、妥当性制約を守るには、対応する属性リスト宣言も必要です。

続いて「終了タグ」の定義です。開始タグの整形式性制約と同じことを言っています。

[Definition: The end of every element that begins with a start-tag MUST be marked by an end-tag containing a name that echoes the element's type as given in the start-tag:]

続いて要素の「内容」の定義です。

[Definition: The text between the start-tag and end-tag is called the element's content:]

これは生成規則を見るより定義を読んだほうがわかりやすく、開始・終了タグに挟まれたテキストのことを「内容」と呼ぶということですね。

さらに、要素が「空」であることの定義と、「空要素タグ」の定義です。

[Definition: An element with no content is said to be empty.]
[Definition: An empty-element tag takes a special form:]

空要素タグを使う以外にも、開始タグの直後に終了タグを配置することでも、空な要素を表現できると理解できます。
相互運用性のため、文書作成者はEMPTYと宣言された要素にのみ空要素タグを使うことが望ましいとされますが、仕様としてはEMPTYな要素型であろうがなかろうが、空要素タグの使用を妨げません。

3.2 Element Type Declarations

要素型宣言についての説明です。

要素型宣言は、ある要素型の子として現れうる要素を制限するものです。
ある宣言が、宣言のない要素型に言及した場合、ユーザの選択によって警告(エラーではない)を報告してもよい、とされています。要素の妥当性制約によって、その要素型の宣言がなければエラーとされますが、その制約との整合性はどうなっているのでしょうか?要素内容モデルとして選択的に現れうると指定された要素の場合、要素として出現はしないが、要素内容モデルには出現することがあるかもしれません(以下のXML文書のchild2など)。そのようなパターンのことを言っているのでしょう。

<!DOCTYPE root [
    <!ELEMENT child1 EMPTY>
    <!-- child2は未宣言だが、文書に出現しない要素なのでエラーではない -->
    <!ELEMENT root (child1|child2)>
]>
<root>
    <child1/>
</root>

また、以下の妥当性制約から、同じ要素型の宣言が重複して現れてはなりません。

Validity constraint: Unique Element Type Declaration
An element type MUST NOT be declared more than once.

3.2.1 Element Content

「要素内容」「内容モデル」の定義です。

[Definition: An element type has element content when elements of that type MUST contain only child elements (no character data), optionally separated by white space (characters matching the nonterminal S).]
[Definition: In this case, the constraint includes a content model, a simple grammar governing the allowed types of the child elements and the order in which they are allowed to appear.]

子要素のみを含む場合に「要素内容」を持つのであって、文字データを含む場合はそうではないようです。文字データを含む場合は、後述の「混合内容」を持ちます。ただし、ホワイトスペースだけは含まれていても構いません。
内容モデルは、子要素の順序・出現回数に制約を与えるものです。内容モデルが曖昧である場合はエラーになります。

内容モデルをパラメタ実体に含める場合にも、適切にネストされていることが要求されます。つまり、対応するカッコは同じ実体の中に含まれている必要があります。

Validity constraint: Proper Group/PE Nesting
Parameter-entity replacement text MUST be properly nested with parenthesized groups. That is to say, if either of the opening or closing parentheses in a choice, seq, or Mixed construct is contained in the replacement text for a parameter entity, both MUST be contained in the same replacement text.
For interoperability, if a parameter-entity reference appears in a choice, seq, or Mixed construct, its replacement text SHOULD contain at least one non-blank character, and neither the first nor last non-blank character of the replacement text SHOULD be a connector (| or ,).

3.2.2 Mixed Content

「混合内容」の定義です。

[Definition: An element type has mixed content when elements of that type may contain character data, optionally interspersed with child elements.]

要素型は、文字データと子要素の両方を含む場合に「混合内容」を持ちます。
なお、混合内容についての制約は、含むことのできる要素型に関するものだけで、順序や出現回数を制約することはできません。なぜ制約できるようにしなかったのでしょうか…その理由は特に記載が見当たらないので不明です。そのような制約が必要であれば、別のスキーマ言語を使用するか、何かしらの要素に文字データを含めるようにモデル自体を変更する必要があるでしょう。

混合内容宣言においては、同じ名前が複数現れてはなりません。

Validity constraint: No Duplicate Types
The same name MUST NOT appear more than once in a single mixed-content declaration.

3.3 Attribute-List Declarations

属性リスト宣言の話です。

以下の定義にもある通り、属性リスト宣言は、要素型に紐づく属性の組を定義し、属性値の制約やデフォルトを指定します。

[Definition: Attribute-list declarations specify the name, data type, and default value (if any) of each attribute associated with a given element type:]

宣言されていない要素型に対する属性リスト宣言はエラーではありませんが、ユーザが選択した場合には警告を出しても構いません。

要素型宣言では重複した宣言は妥当性制約違反となりますが、同じ要素型に関する属性リスト宣言については制約違反でもエラーでもなく、全てマージされたうえで最初に現れたもの以外を無視します
ただし、同じ要素型に対する複数の属性リスト宣言や重複した属性の定義がある場合、ユーザが選択した場合に警告を出しても構いません。

3.3.1 Attribute Types

属性値に対する制約を表す型です。制約は属性値が正規化された後の状態でチェックされます。

属性型には文字列型、トークン化対象型、列挙型の3つがあります。文字列型はただの文字列であり、3.1章に述べられた点以外は特に制約されませんが、ほか2つはそれぞれ追加の制約が課せられます。

まずトークン化対象型についてです。
トークン化対象型にはID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN, NMTOKENSの7個があり、それぞれ以下のような妥当性制約が課せられています。

Validity constraint: ID
Values of type ID MUST match the Name production. A name MUST NOT appear more than once in an XML document as a value of this type; i.e., ID values MUST uniquely identify the elements which bear them.
Validity constraint: One ID per Element Type
An element type MUST NOT have more than one ID attribute specified.
Validity constraint: ID Attribute Default
An ID attribute MUST have a declared default of #IMPLIED or #REQUIRED.
Validity constraint: IDREF
Values of type IDREF MUST match the Name production, and values of type IDREFS MUST match Names; each Name MUST match the value of an ID attribute on some element in the XML document; i.e. IDREF values MUST match the value of some ID attribute.
Validity constraint: Entity Name
Values of type ENTITY MUST match the Name production, values of type ENTITIES MUST match Names; each Name MUST match the name of an unparsed entity declared in the DTD.
Validity constraint: Name Token
Values of type NMTOKEN MUST match the Nmtoken production; values of type NMTOKENS MUST match Nmtokens.

はじめの3つはIDに関する制約で、Name(生成規則[5])にマッチすること、文書中で一意であること、一要素に高々1回しか現れてはならないこと、属性デフォルトは#IMPLIEDか#REQUIREDでなければならないことが述べられています。
最後のは要するにデフォルト値は設定するなということですね。

4つ目はIDREF, IDREFSに関する制約で、それぞれName, Names(生成規則[6])にマッチすること、それぞれのNameが文書中のあるIDの値にマッチしなければならないことが述べられています。
実装の話になりますが、これってどのタイミングで検査すればいいのでしょうか?自身よりあとで現れたIDを指すIDREF(S)も当然あるわけで、これは文書を最後まで読みきらないと検査できませんよね。すると、エラーを出せるタイミングも文書を読み切った後なわけで、エラー発報のタイミングがおかしなことになります。
これは実装によりけりになるのでしょうが、例えば文書を全部読み切ってから妥当性検証を開始するとか、発報タイミングは妥協するとかが考えられます。libxml2は妥協して読み切ってからやってしまっているようです。

5つ目はENTITY, ENTITIESに関する制約で、それぞれName, Namesにマッチすること、それぞれのNameがDTDで定義された解析対象外実体の名前にマッチすることが述べられています。
解析対象外実体の名前であることが重要な点かもしれません。

6つ目はNMTOKEN, NMTOKENSに関する制約で、それぞれNmtoken(生成規則[7]), Nmtokens(生成規則[8])にマッチすることが述べられています。

続いては列挙型についてです。
定義のとおりですが、許可された値にマッチしなければならないという制約が加えられます。

[Definition: Enumerated attributes have a list of allowed values in their declaration ]

列挙型はさらに、記法を列挙するかNmtokenを列挙するかで2通りあり、それぞれリストするNameやNmtokenに関する妥当性制約が課せられます。

Validity constraint: Notation Attributes
Values of this type MUST match one of the notation names included in the declaration; all notation names in the declaration MUST be declared.
Validity constraint: One Notation Per Element Type
An element type MUST NOT have more than one NOTATION attribute specified.
Validity constraint: No Notation on Empty Element
For compatibility, an attribute of type NOTATION MUST NOT be declared on an element declared EMPTY.
Validity constraint: No Duplicate Tokens
The notation names in a single NotationType attribute declaration, as well as the NmTokens in a single Enumeration attribute declaration, MUST all be distinct.
Validity constraint: Enumeration
Values of this type MUST match one of the Nmtoken tokens in the declaration.

はじめの3つは記法を列挙する場合についての制約で、属性値が列挙されたNameのいずれか1つにマッチすること、列挙されたNameがすべてDTDで宣言された記法のいずれか1つにマッチすること、1つの要素につき高々1つしか記法属性が現れてはならないこと、EMPTYな要素に記法属性が現れてはならないことが述べられています。
どうでもいい話ですが、3つ目について和文版では「EMPTYと宣言された属性に対して宣言されてはならない」と書いてあります。原文からすると「属性」ではなく「要素」ですよね。ここまできて初めて誤りと思われる訳を発見しました。

4つ目はいずれの列挙型属性にも当てはまる制約で、列挙された値に重複があってはならないことが述べられています。

5つ目はNmtokenを列挙する場合についての制約ですが、列挙されたNmtokenのいずれかにマッチする値しか使ってはならないという当たり前のことが書いてあるだけです。

3.3.2 Attribute Defaults

「属性デフォルト」の話です。

「属性デフォルト」は、その属性が必須か否か、必須でないならその属性がないときにXMLがどう対応すべきか、の情報を提供するものであると述べられています。

属性宣言は#REQUIRED, #IMPLIEDが指定されるか、#FIXEDが付与されたデフォルト値を持つか、付与されないデフォルト値を持つか、の4パターンあります。
#REQUIREDはその属性が常に指定されなければならないこと、#IMPLIEDはデフォルト値が提供されない(かつ、必須ではない)ことを示します。
それ以外であるとき、デフォルト値が提供されます。#FIXEDが付与されている場合には、属性を指定するときは常に提供されたデフォルト値を持たなければなりません。つまり、属性を指定しないか、デフォルト値を同じ属性値を指定するか、の二択になるということです。

[Definition: If the declaration is neither #REQUIRED nor #IMPLIED, then the AttValue value contains the declared default value; the #FIXED keyword states that the attribute MUST always have the default value. When an XML processor encounters an element without a specification for an attribute for which it has read a default value declaration, it MUST report the attribute with the declared default value to the application.]

続いて、妥当性制約も与えられています。

Validity constraint: Required Attribute
If the default declaration is the keyword #REQUIRED, then the attribute MUST be specified for all elements of the type in the attribute-list declaration.

Validity constraint: Attribute Default Value Syntactically Correct
The declared default value MUST meet the syntactic constraints of the declared attribute type. That is, the default value of an attribute:

  • of type IDREF or ENTITY must match the Name production;
  • of type IDREFS or ENTITIES must match the Names production;
  • of type NMTOKEN must match the Nmtoken production;
  • of type NMTOKENS must match the Nmtokens production;
  • of an enumerated type (either a NOTATION type or an enumeration) must match one of the enumerated values.

Note that only the syntactic constraints of the type are required here; other constraints (e.g. that the value be the name of a declared unparsed entity, for an attribute of type ENTITY) will be reported by a validating parser only if an element without a specification for this attribute actually occurs.

Validity constraint: Fixed Attribute Default
If an attribute has a default value declared with the #FIXED keyword, instances of that attribute MUST match the default value.

1つ目は#REQUIREDの意味するところを述べているだけです。
2つ目はデフォルト値として指定される値が、"3.3.1 Attribute Types"で述べられたそれぞれの属性型の制約を満たす値でなければならないということと述べています。
3つ目は#FIXEDの意味するところを述べています。この文章だけだと、#FIXEDな属性は常に指定されなければならないのか?という気がしなくもないですが、そうではなく、指定される場合にはそのデフォルト値を属性値として持たなければならないということに過ぎません。

3.3.3 Attribute-Value Normalization

属性値は、アプリケーションに渡されたり、妥当性を検証される前に、「正規化」されなければならないと述べられています。

「正規化」のアルゴリズムは以下のようなものです。

  1. "2.11 End-of-Line Handling"で述べられた行末処理を実施する。
  2. 空文字列から成る正規化済値を用意する。
  3. 未正規化値の最初から最後まで順に、文字、実体参照文字参照それぞれについて以下のように処理する。

    1. 文字参照について、参照された文字を正規化済値に追加する。
    2. 実体参照について、このアルゴリズムのステップ3を、実体の置換テキストに再帰的に適用する。
    3. ホワイトスペース文字について、"#x20"(スペース文字)を正規化済値に追加する。
    4. その他の文字について、その文字をそのまま正規化済値に追加する。
  4. 属性型がCDATAでなければ、先頭・末尾の全ての"\x20"を除去し、他の連続する"#x20"を1つの"#x20"に置換する。

ステップ3.のa.について、参照された文字が"#x20"以外のスペース文字であったとき、たとえそれが"#xD"("\r")であったとしても、そのまま正規化済値に追加されます。結果として、正規化済値が"#x20"以外のホワイトスペースを含むことはありうるということです。
ステップ3.のb.については、単に再帰的に処理されるので、スペース文字を含む場合は"#x20"に置換されますし、"#x20"以外を指す文字参照を含むのであればその文字参照に参照される文字がそのまま追加されるということになります。
要するに、ホワイトスペースへの参照が文字参照であるか実体参照であるかによって、ホワイトスペースの置換が起こるか否かが変わるということですね。

また、ステップ4.で除去・置換されるのは"#x20"であって、ホワイトスペースではないため、"#xA", "#x9", "#xD"は除去・置換されません

妥当性を検証しないXMLプロセッサであれば、宣言が読み取られなかった属性はすべてCDATAとして扱われることが望ましいとされています。
また、宣言が読み取られなかった実体への参照を含む場合はエラーです。

上記のアルゴリズムを素朴に実装すると、以下のようになるでしょう。

// 文字参照の解決
fn resolve_charref(reference: String) -> char { todo!() }
// 実体参照の解決
fn resolve_entref(reference: String) -> String { todo!() }
fn normalize_attribute_value(s: &str, is_cdata: bool) -> String {
    // ステップ1.
    let s = s.replace("\r\n", "\n").replace("\r", "\n");
    // ステップ2.
    let mut buf = String::new();
    // ステップ3.開始
    normalize_internal(&s, &mut buf);

    fn normalize_internal(s: &str, buf: &mut String) {
        let mut chars = s.chars().peekable();

        while let Some(start) = chars.next() {
            match start {
                '&' => {
                    let mut reference = String::new();
                    while let Some(c) = chars.next().filter(|&n| n != ';') {
                        reference.push(c);
                    }
                    if reference.starts_with('#') {
                        // ステップ3.a.
                        buf.push(resolve_charref(reference));
                    } else {
                        // ステップ3.b.
                        normalize_internal(&resolve_entref(reference), buf);
                    }
                }
                // ステップ3.c.
                '\x20' | '\x09' | '\x0A' | '\x0D' => buf.push('\x20'),
                // ステップ3.d.
                c => buf.push(c),
            }
        }
    }

    // ステップ4.
    if !is_cdata {
        let trimed = buf.trim_matches(|c| c == '\x20');
        let mut chunks = trimed
            .split(|c| c == '\x20')
            .filter(|s| !s.is_empty());
        let mut normalized = chunks
            .next()
            .map(|s| s.to_owned())
            .unwrap_or_default();
        for chunk in chunks {
            normalized.push('\x20');
            normalized.push_str(chunk);
        }
        buf = normalized;
    }
    buf
}

3.4 Conditional Sections

「条件付きセクション」の定義です。

[Definition: Conditional sections are portions of the document type declaration external subset or of external parameter entities which are included in, or excluded from, the logical structure of the DTD based on the keyword which governs them.]

条件付きセクションを使用することで、DTDの内容を動的に変更することができます。
構文としては静的なものであるため、条件付きセクション単体で動的な定義を実現することはできなくて、冒頭のINCLUDE, IGNOREを含む範囲をパラメタ実体で記述し、パラメタ実体の定義を切り替えることで実現します。
内部サブセットの一部だけをパラメタ実体で記述することができないという制約との関連もあるのかとは思いますが、extSubsetDecl(生成規則[31])の内部でしか使用できないため、外部サブセットでのみ利用可能な機能です。

条件付きセクションも他のマークアップ宣言同様、パラメタ実体に含まれる際には適切にネストする必要があるという妥当性制約が課されています。

Validity constraint: Proper Conditional Section/PE Nesting
If any of the "<![", "[", or "]]>" of a conditional section is contained in the replacement text for a parameter-entity reference, all of them MUST be contained in the same replacement text.

IGNOREを指定されたセクションの内容は、DTDとして解析されてはならず、パラメタ実体も認識されてはなりません。たとえINCLUDEを指定されたセクションだとしても、IGNOREを指定されたセクションに含まれる限り、解析してはなりません。
ただし、条件付きセクションの開始・終了区切り子である"<!["と"]]>"は例外である、と述べられています。これは生成規則[64], 生成規則[65]とあわせて見ないと意味不明だと思いますが、条件付きセクション開始・終了区切り子の組だけは認識しなければならないということです。この規則がないと、IGNOREにINCLUDEを入れ子したとき、正しく条件付きセクションを解析できなくなります。(内側のINCLUDEを指定されたセクションの終了区切り子が誤って外側のセクションの終了と認識されてしまう)

INCLUDE, IGNOREのキーワードがパラメタ実体に含まれる場合、その条件付きセクションが無視されるか否かを決定する前にパラメタ実体は展開されなければなりません。

あとがき

今回も最後まで行けませんでした。3章長いです。
ただ、4章は見た感じもっと長いです。私にとって鬼門と言える、実体参照の展開がこの章に含まれています。

6章はたいした内容がないので、5章を次に含めるかまた分けるかは悩ましいところです。