diff --git a/CHANGELOG.md b/CHANGELOG.md index 973cfbe..2edeb2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- RELAX NG compact `>>` follow-annotations (#79), completing the RNC syntax. A `>>` annotation after a pattern (a foreign element name with optional `[ … ]` content) is skipped, chaining and combining with leading `[ … ]` annotations; both carry no schema semantics. (Namespace declarations inside annotations are already dropped by the annotation skipper.) - XSLT `document()` fragments and base-URI resolution (#82), completing the XSLT function/output audit. A `#fragment` selects a subset of the loaded document via XPointer, a node-set first argument unions the documents named by each node, and a relative URI is resolved against a caller-supplied `baseURI` (also applied to `xsl:include`/`xsl:import`). - XSLT result-tree fragments usable as node-sets (#82). An `xsl:variable` with a body now binds a queryable document fragment instead of a plain string, so `count($rtf/*)`, `$rtf/child`, and `xsl:for-each select="$rtf/..."` work; it still has its concatenated text as a string value. - XSLT `xsl:output cdata-section-elements` (#82). The text content of the named result elements is emitted in `` sections instead of escaped, leaving nested elements and other elements unaffected. diff --git a/Sources/Schema/RelaxNGCompactParser.swift b/Sources/Schema/RelaxNGCompactParser.swift index 6d66d52..c2972ff 100644 --- a/Sources/Schema/RelaxNGCompactParser.swift +++ b/Sources/Schema/RelaxNGCompactParser.swift @@ -192,14 +192,28 @@ extension RNCParser { private func parseParticle() -> Pattern { let primary = parsePrimary() + let particle: Pattern switch peek() { case .symbol("?"): advance() - return .choice(primary, .empty) + particle = .choice(primary, .empty) case .symbol("*"): advance() - return .choice(.oneOrMore(primary), .empty) + particle = .choice(.oneOrMore(primary), .empty) case .symbol("+"): advance() - return .oneOrMore(primary) - default: return primary + particle = .oneOrMore(primary) + default: particle = primary + } + skipFollowAnnotations() + return particle + } + + /// Skips `>>` follow-annotations after a pattern: each is a foreign element + /// name with optional `[ … ]` content, carrying no schema semantics. + private func skipFollowAnnotations() { + while peek() == .symbol(">"), peek(1) == .symbol(">") { + advance() + advance() + if case .word = peek() { advance() } + skipAnnotation() } } diff --git a/Tests/RelaxNGCompactFollowAnnotationTests.swift b/Tests/RelaxNGCompactFollowAnnotationTests.swift new file mode 100644 index 0000000..679e76c --- /dev/null +++ b/Tests/RelaxNGCompactFollowAnnotationTests.swift @@ -0,0 +1,38 @@ +@testable import PureXML +import Testing + +@Suite("RELAX NG compact >> follow-annotations") +struct RelaxNGCompactFollowAnnotationTests { + private func valid(_ rnc: String, _ xml: String) throws -> Bool { + try PureXML.Schema.RelaxNG(compact: rnc).validate(xml) + } + + @Test("A follow-annotation after an element is ignored") + func test_afterElement() throws { + let rnc = "element root { element a { empty } >> a:documentation [ \"a child\" ] }" + #expect(try valid(rnc, "")) + #expect(try !valid(rnc, "")) + } + + @Test("A follow-annotation on a define's pattern is ignored") + func test_onDefine() throws { + let rnc = """ + start = element root { ref-a } + ref-a = element a { empty } >> a:doc [ "the a element" ] + """ + #expect(try valid(rnc, "")) + } + + @Test("Follow-annotations chain after a particle") + func test_chained() throws { + let rnc = "element root { element a { empty } >> a:x [ \"1\" ] >> a:y [ \"2\" ], element b { empty } }" + #expect(try valid(rnc, "")) + #expect(try !valid(rnc, "")) + } + + @Test("A leading and a follow annotation can both appear") + func test_leadingAndFollow() throws { + let rnc = "element root { [ a:doc [ \"lead\" ] ] element a { empty } >> a:doc [ \"follow\" ] }" + #expect(try valid(rnc, "")) + } +}