diff --git a/README.md b/README.md
index 704fa4a..bab0674 100644
--- a/README.md
+++ b/README.md
@@ -88,7 +88,7 @@ flowchart TB
OutEnc["#106 output encodings: complete"]:::done
StreamVal["#107 streaming validation: DTD, RELAX NG, XSD"]:::done
MoreEnc["#108 encodings: ISO-2022-JP, EUC-TW"]:::done
- Depth["Tier 2 conformance corpora; Tier 3 DTD PE depth (C14N 1.1, EXSLT done); frontier #104"]:::todo
+ Depth["Tier 3 done (C14N 1.1, EXSLT, DTD PE depth); Tier 2 conformance corpora; frontier #104"]:::todo
Epic105 --> OutEnc
OutEnc --> StreamVal
StreamVal --> MoreEnc
@@ -99,7 +99,7 @@ flowchart TB
PureXML is a working, dependency-free XML library today: parse, emit, validate,
query, and stream documents on macOS, Linux, Windows, and WASM. The test suite
-currently runs **801 tests in 123 suites** (`swift test`).
+currently runs **805 tests in 124 suites** (`swift test`).
### Shipped (libxml2-aligned surface)
diff --git a/Tests/DTDParameterEntityDepthTests.swift b/Tests/DTDParameterEntityDepthTests.swift
new file mode 100644
index 0000000..0c92067
--- /dev/null
+++ b/Tests/DTDParameterEntityDepthTests.swift
@@ -0,0 +1,61 @@
+@testable import PureXML
+import Testing
+
+@Suite("DTD parameter-entity and external-subset depth")
+struct DTDParameterEntityDepthTests {
+ private let dtdAllowed = PureXML.Parsing.Limits(allowDoctype: true)
+
+ private func documentType(_ xml: String, resolver: PureXML.Parsing.EntityResolver = .refusing) throws -> PureXML.Parsing.DocumentType {
+ try PureXML.Parsing.Parser().parseWithDocumentType(xml, limits: dtdAllowed, resolver: resolver).documentType
+ }
+
+ private func rootText(_ node: PureXML.Model.Node) -> String {
+ guard case let .document(children) = node else { return "" }
+ for case let .element(element) in children {
+ return element.text
+ }
+ return ""
+ }
+
+ @Test("A parameter entity expands within an element-model declaration")
+ func test_parameterEntityInElementModel() throws {
+ let dtd = try documentType("]>")
+ #expect(dtd.elementModels["r"] == "(a|b)")
+ }
+
+ @Test("A parameter entity expands within an attribute-list declaration")
+ func test_parameterEntityInAttlist() throws {
+ let xml = "]>"
+ // The attribute is declared via the PE, so a missing required code is caught.
+ let errors = try PureXML.validateAgainstInternalDTD(xml)
+ #expect(errors.contains { $0.reason.contains("code") })
+ }
+
+ @Test("Deeply nested parameter entities compose fully")
+ func test_deepNesting() throws {
+ let xml = """
+
+
+
+
+
+ ]>&e;
+ """
+ try #expect(rootText(PureXML.parse(xml, limits: dtdAllowed)) == "12345")
+ }
+
+ @Test("An external subset can itself load a further external parameter entity")
+ func test_recursiveExternalLoading() throws {
+ let resolver = PureXML.Parsing.EntityResolver(resolveExternalSubset: { id in
+ switch id.systemID {
+ case "a.dtd": "%inner;"
+ case "b.dtd": ""
+ default: nil
+ }
+ })
+ let xml = "%ext;]>&deep;"
+ let node = try PureXML.parse(xml, limits: dtdAllowed, resolver: resolver)
+ #expect(rootText(node) == "found")
+ }
+}