feat(relaxng): skip RNC >> follow-annotations, completing #79

A >> follow-annotation after a pattern (a foreign element name with
optional [ ... ] content) is skipped, chaining and combining with leading
annotations; neither carries schema semantics. Namespace declarations
inside annotations were already dropped by the annotation skipper, so the
RNC compact syntax is now fully covered.
This commit is contained in:
Mihaela Mihaljevic 2026-06-09 03:20:47 +02:00
parent 45001f3422
commit dbdf98e231
3 changed files with 57 additions and 4 deletions

View File

@ -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 `<![CDATA[…]]>` sections instead of escaped, leaving nested elements and other elements unaffected.

View File

@ -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()
}
}

View File

@ -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, "<root><a/></root>"))
#expect(try !valid(rnc, "<root><b/></root>"))
}
@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, "<root><a/></root>"))
}
@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, "<root><a/><b/></root>"))
#expect(try !valid(rnc, "<root><a/></root>"))
}
@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, "<root><a/></root>"))
}
}