diff --git a/Sources/Schema/RelaxNGCompactParser.swift b/Sources/Schema/RelaxNGCompactParser.swift
index 8ce5528..cbaaf9a 100644
--- a/Sources/Schema/RelaxNGCompactParser.swift
+++ b/Sources/Schema/RelaxNGCompactParser.swift
@@ -229,11 +229,29 @@ private final class RNCParser {
advance()
guard word.contains(":") else { return .ref(word) }
let type = PureXML.Schema.BuiltinType(rawValue: strip(word)) ?? .string
- if peek() == .symbol("{") { skipBraces() }
+ var facets = PureXML.Schema.Facets()
+ if peek() == .symbol("{") { parseParams(into: &facets) }
if case let .string(value) = peek() { advance()
return .value(value)
}
- return .data(PureXML.Schema.SimpleType(base: type))
+ return .data(PureXML.Schema.SimpleType(base: type, facets: facets))
+ }
+
+ /// Parses a datatype parameter block `{ name = "value" ... }` into facets (the
+ /// compact-syntax form of ``).
+ private func parseParams(into facets: inout PureXML.Schema.Facets) {
+ advance() // {
+ while position < tokens.count, peek() != .symbol("}") {
+ guard case let .word(name) = peek() else { advance()
+ continue
+ }
+ advance()
+ if peek() == .symbol("=") { advance() }
+ guard case let .string(value) = peek() else { continue }
+ advance()
+ PureXML.Schema.RelaxNGFacets.apply(name, value, into: &facets)
+ }
+ if peek() == .symbol("}") { advance() }
}
private func parseElement() -> Pattern {
@@ -353,18 +371,6 @@ private extension RNCParser {
return ""
}
- private func skipBraces() {
- guard peek() == .symbol("{") else { return }
- var depth = 0
- repeat {
- switch advance() {
- case .symbol("{"): depth += 1
- case .symbol("}"): depth -= 1
- default: break
- }
- } while depth > 0 && position < tokens.count
- }
-
private func strip(_ qualified: String) -> String {
qualified.split(separator: ":").last.map(String.init) ?? qualified
}
diff --git a/Sources/Schema/RelaxNGFacets.swift b/Sources/Schema/RelaxNGFacets.swift
new file mode 100644
index 0000000..26f4a5c
--- /dev/null
+++ b/Sources/Schema/RelaxNGFacets.swift
@@ -0,0 +1,33 @@
+extension PureXML.Schema {
+ /// Builds the XSD-style ``Facets`` a RELAX NG datatype constrains its base type
+ /// with, from `value` pairs (XML syntax) or `name "value"` pairs
+ /// (compact syntax). Shared by both RELAX NG parsers.
+ enum RelaxNGFacets {
+ static func apply(_ name: String, _ value: String, into facets: inout Facets) {
+ applyValue(name, value, into: &facets)
+ applyNumeric(name, Int(value), into: &facets)
+ }
+
+ private static func applyValue(_ name: String, _ value: String, into facets: inout Facets) {
+ switch name {
+ case "pattern": facets.patterns.append(value)
+ case "minInclusive": facets.minInclusive = value
+ case "maxInclusive": facets.maxInclusive = value
+ case "minExclusive": facets.minExclusive = value
+ case "maxExclusive": facets.maxExclusive = value
+ default: break
+ }
+ }
+
+ private static func applyNumeric(_ name: String, _ number: Int?, into facets: inout Facets) {
+ switch name {
+ case "length": facets.length = number
+ case "minLength": facets.minLength = number
+ case "maxLength": facets.maxLength = number
+ case "totalDigits": facets.totalDigits = number
+ case "fractionDigits": facets.fractionDigits = number
+ default: break
+ }
+ }
+ }
+}
diff --git a/Sources/Schema/RelaxNGParser.swift b/Sources/Schema/RelaxNGParser.swift
index eefdbca..f7ee55e 100644
--- a/Sources/Schema/RelaxNGParser.swift
+++ b/Sources/Schema/RelaxNGParser.swift
@@ -122,39 +122,12 @@ private final class RNGCompiler {
var facets = PureXML.Schema.Facets()
for param in RNGNode.children(node, named: "param") {
if let name = RNGNode.attribute(param, "name") {
- Self.applyParam(name, RNGNode.text(param), into: &facets)
+ PureXML.Schema.RelaxNGFacets.apply(name, RNGNode.text(param), into: &facets)
}
}
return PureXML.Schema.SimpleType(base: base, facets: facets)
}
- private static func applyParam(_ name: String, _ value: String, into facets: inout PureXML.Schema.Facets) {
- applyValueParam(name, value, into: &facets)
- applyNumericParam(name, Int(value), into: &facets)
- }
-
- private static func applyValueParam(_ name: String, _ value: String, into facets: inout PureXML.Schema.Facets) {
- switch name {
- case "pattern": facets.patterns.append(value)
- case "minInclusive": facets.minInclusive = value
- case "maxInclusive": facets.maxInclusive = value
- case "minExclusive": facets.minExclusive = value
- case "maxExclusive": facets.maxExclusive = value
- default: break
- }
- }
-
- private static func applyNumericParam(_ name: String, _ number: Int?, into facets: inout PureXML.Schema.Facets) {
- switch name {
- case "length": facets.length = number
- case "minLength": facets.minLength = number
- case "maxLength": facets.maxLength = number
- case "totalDigits": facets.totalDigits = number
- case "fractionDigits": facets.fractionDigits = number
- default: break
- }
- }
-
/// Resolves an `externalRef` to the referenced schema's pattern, merging any
/// grammar `define`s it carries into this compiler's table.
private func externalRef(_ node: Tree) -> Pattern {
diff --git a/Tests/RelaxNGCompactParamTests.swift b/Tests/RelaxNGCompactParamTests.swift
new file mode 100644
index 0000000..59eb45b
--- /dev/null
+++ b/Tests/RelaxNGCompactParamTests.swift
@@ -0,0 +1,38 @@
+@testable import PureXML
+import Testing
+
+@Suite("RELAX NG compact datatype parameter blocks")
+struct RelaxNGCompactParamTests {
+ private func valid(_ rnc: String, _ xml: String) throws -> Bool {
+ try PureXML.Schema.RelaxNG(compact: rnc).validate(xml)
+ }
+
+ @Test("A minLength param block constrains a data value")
+ func test_minLength() throws {
+ let rnc = "element code { xsd:string { minLength = \"3\" } }"
+ #expect(try valid(rnc, "abc"))
+ #expect(try !valid(rnc, "ab"))
+ }
+
+ @Test("A maxInclusive param block constrains a numeric value")
+ func test_maxInclusive() throws {
+ let rnc = "element n { xsd:integer { maxInclusive = \"10\" } }"
+ #expect(try valid(rnc, "5"))
+ #expect(try !valid(rnc, "20"))
+ }
+
+ @Test("Multiple params in one block all apply")
+ func test_multipleParams() throws {
+ let rnc = "element code { xsd:string { minLength = \"2\" maxLength = \"4\" } }"
+ #expect(try valid(rnc, "abc"))
+ #expect(try !valid(rnc, "a"))
+ #expect(try !valid(rnc, "abcde"))
+ }
+
+ @Test("A datatype with no param block still validates against its base")
+ func test_noParams() throws {
+ let rnc = "element n { xsd:integer }"
+ #expect(try valid(rnc, "42"))
+ #expect(try !valid(rnc, "x"))
+ }
+}