diff --git a/Sources/Schema/RelaxNGCompactHelpers.swift b/Sources/Schema/RelaxNGCompactHelpers.swift
index 3d9c2e3..878d820 100644
--- a/Sources/Schema/RelaxNGCompactHelpers.swift
+++ b/Sources/Schema/RelaxNGCompactHelpers.swift
@@ -98,11 +98,24 @@ extension RNCParser {
func expectString() -> String {
if case let .string(value) = peek() { advance()
- return value
+ return concatenated(value)
}
return ""
}
+ /// Folds RNC string concatenation: `"a" ~ "b"` is the single literal `"ab"`,
+ /// the way long strings are split across lines. The leading string has been
+ /// consumed; this appends each following `~ "…"`.
+ func concatenated(_ value: String) -> String {
+ var result = value
+ while peek() == .symbol("~"), case let .string(next)? = peek(1) {
+ advance()
+ advance()
+ result += next
+ }
+ return result
+ }
+
func strip(_ qualified: String) -> String {
qualified.split(separator: ":").last.map(String.init) ?? qualified
}
diff --git a/Sources/Schema/RelaxNGCompactParser.swift b/Sources/Schema/RelaxNGCompactParser.swift
index 0bd1f61..6d66d52 100644
--- a/Sources/Schema/RelaxNGCompactParser.swift
+++ b/Sources/Schema/RelaxNGCompactParser.swift
@@ -250,7 +250,7 @@ extension RNCParser {
switch peek() {
case let .string(value): advance()
// A bare string literal is shorthand for a `token`-typed value.
- return .value(PureXML.Schema.SimpleType(base: .token), value)
+ return .value(PureXML.Schema.SimpleType(base: .token), concatenated(value))
case .symbol("("): advance()
let pattern = parsePattern()
expectSymbol(")")
@@ -269,7 +269,7 @@ extension RNCParser {
var facets = PureXML.Schema.Facets()
if peek() == .symbol("{") { parseParams(into: &facets) }
if case let .string(value) = peek() { advance()
- return .value(PureXML.Schema.SimpleType(base: type), value)
+ return .value(PureXML.Schema.SimpleType(base: type), concatenated(value))
}
return .data(PureXML.Schema.SimpleType(base: type, facets: facets))
}
diff --git a/Tests/RelaxNGCompactConcatTests.swift b/Tests/RelaxNGCompactConcatTests.swift
new file mode 100644
index 0000000..b2e78f6
--- /dev/null
+++ b/Tests/RelaxNGCompactConcatTests.swift
@@ -0,0 +1,33 @@
+@testable import PureXML
+import Testing
+
+@Suite("RELAX NG compact string concatenation")
+struct RelaxNGCompactConcatTests {
+ private func validRNC(_ rnc: String, _ xml: String) throws -> Bool {
+ try PureXML.Schema.RelaxNG(compact: rnc).validate(xml)
+ }
+
+ @Test("A value literal split with ~ is concatenated")
+ func test_valueConcatenation() throws {
+ let rnc = "element greeting { \"hello \" ~ \"world\" }"
+ #expect(try validRNC(rnc, "hello world"))
+ #expect(try !validRNC(rnc, "hello"))
+ }
+
+ @Test("More than two segments concatenate left to right")
+ func test_multiSegment() throws {
+ let rnc = "element n { \"a\" ~ \"b\" ~ \"c\" }"
+ #expect(try validRNC(rnc, "abc"))
+ #expect(try !validRNC(rnc, "ab"))
+ }
+
+ @Test("A namespace URI may be assembled with ~")
+ func test_namespaceConcatenation() throws {
+ let rnc = """
+ namespace p = "urn:" ~ "example"
+ element root { element p:item { empty } }
+ """
+ #expect(try validRNC(rnc, ""))
+ #expect(try !validRNC(rnc, ""))
+ }
+}