Surface the parsed <!DOCTYPE> internal subset's <!ELEMENT> declarations from the parser and validate a tree against them. Reimplemented from the DTD content-model spec; no upstream code copied. - DoctypeScanner captures <!ELEMENT name model> declarations; the parser surfaces them as Parsing.DocumentType (via parseWithDocumentType). - Validation.ContentModel + ContentModelParser parse a model string (EMPTY, ANY, (#PCDATA), mixed, and element content with sequence/choice and ?/*/+). - ContentModelMatcher matches the child element-name sequence against a model as a regular language (NFA-position matching). - Validation.DTDSchema.validate reports issues for EMPTY/PCDATA/mixed violations, element content with stray text, and child sequences that do not match. - PureXML.validateAgainstInternalDTD(_:) parses and validates in one call. - ATTLIST validation is out of scope. 9 tests; 70 total.
75 lines
3.2 KiB
Swift
75 lines
3.2 KiB
Swift
@testable import PureXML
|
|
import Testing
|
|
|
|
@Suite("DTD validation")
|
|
struct DTDValidationTests {
|
|
private func issues(_ xml: String, strict: Bool = false) throws -> [PureXML.Validation.Issue] {
|
|
try PureXML.validateAgainstInternalDTD(xml, strict: strict)
|
|
}
|
|
|
|
@Test("EMPTY element accepts no content and rejects content")
|
|
func test_empty() throws {
|
|
try #expect(issues("<!DOCTYPE r [<!ELEMENT r EMPTY>]><r/>").isEmpty)
|
|
try #expect(!issues("<!DOCTYPE r [<!ELEMENT r EMPTY>]><r>x</r>").isEmpty)
|
|
try #expect(!issues("<!DOCTYPE r [<!ELEMENT r EMPTY><!ELEMENT a EMPTY>]><r><a/></r>").isEmpty)
|
|
}
|
|
|
|
@Test("(#PCDATA) accepts text and rejects child elements")
|
|
func test_pcdata() throws {
|
|
try #expect(issues("<!DOCTYPE r [<!ELEMENT r (#PCDATA)>]><r>hello</r>").isEmpty)
|
|
try #expect(!issues("<!DOCTYPE r [<!ELEMENT r (#PCDATA)><!ELEMENT a EMPTY>]><r><a/></r>").isEmpty)
|
|
}
|
|
|
|
@Test("Sequence content model enforces order and presence")
|
|
func test_sequence() throws {
|
|
let dtd = "<!DOCTYPE r [<!ELEMENT r (a,b)><!ELEMENT a EMPTY><!ELEMENT b EMPTY>]>"
|
|
try #expect(issues("\(dtd)<r><a/><b/></r>").isEmpty)
|
|
try #expect(!issues("\(dtd)<r><b/><a/></r>").isEmpty)
|
|
try #expect(!issues("\(dtd)<r><a/></r>").isEmpty)
|
|
}
|
|
|
|
@Test("Choice content model accepts exactly one alternative")
|
|
func test_choice() throws {
|
|
let dtd = "<!DOCTYPE r [<!ELEMENT r (a|b)><!ELEMENT a EMPTY><!ELEMENT b EMPTY>]>"
|
|
try #expect(issues("\(dtd)<r><a/></r>").isEmpty)
|
|
try #expect(issues("\(dtd)<r><b/></r>").isEmpty)
|
|
try #expect(!issues("\(dtd)<r><a/><b/></r>").isEmpty)
|
|
}
|
|
|
|
@Test("Occurrence indicators bound repetition")
|
|
func test_occurrence() throws {
|
|
let star = "<!DOCTYPE r [<!ELEMENT r (a)*><!ELEMENT a EMPTY>]>"
|
|
try #expect(issues("\(star)<r></r>").isEmpty)
|
|
try #expect(issues("\(star)<r><a/><a/><a/></r>").isEmpty)
|
|
let plus = "<!DOCTYPE r [<!ELEMENT r (a)+><!ELEMENT a EMPTY>]>"
|
|
try #expect(!issues("\(plus)<r></r>").isEmpty)
|
|
let optional = "<!DOCTYPE r [<!ELEMENT r (a?,b)><!ELEMENT a EMPTY><!ELEMENT b EMPTY>]>"
|
|
try #expect(issues("\(optional)<r><b/></r>").isEmpty)
|
|
}
|
|
|
|
@Test("Mixed content allows only the declared elements")
|
|
func test_mixed() throws {
|
|
let dtd = "<!DOCTYPE r [<!ELEMENT r (#PCDATA|a)*><!ELEMENT a EMPTY><!ELEMENT b EMPTY>]>"
|
|
try #expect(issues("\(dtd)<r>text<a/>more</r>").isEmpty)
|
|
try #expect(!issues("\(dtd)<r><b/></r>").isEmpty)
|
|
}
|
|
|
|
@Test("Element content rejects character data")
|
|
func test_elementContentRejectsText() throws {
|
|
let dtd = "<!DOCTYPE r [<!ELEMENT r (a)><!ELEMENT a EMPTY>]>"
|
|
try #expect(!issues("\(dtd)<r>text<a/></r>").isEmpty)
|
|
}
|
|
|
|
@Test("Strict mode rejects undeclared elements")
|
|
func test_strictUndeclared() throws {
|
|
let dtd = "<!DOCTYPE r [<!ELEMENT r ANY>]>"
|
|
try #expect(issues("\(dtd)<r><undeclared/></r>", strict: false).isEmpty)
|
|
try #expect(!issues("\(dtd)<r><undeclared/></r>", strict: true).isEmpty)
|
|
}
|
|
|
|
@Test("A document without a DTD reports no content-model issues")
|
|
func test_noDTD() throws {
|
|
try #expect(issues("<r><a/></r>").isEmpty)
|
|
}
|
|
}
|