diff --git a/Sources/HTML/HTMLDocumentBuilder.swift b/Sources/HTML/HTMLDocumentBuilder.swift
index 3cbba6c..bee4d16 100644
--- a/Sources/HTML/HTMLDocumentBuilder.swift
+++ b/Sources/HTML/HTMLDocumentBuilder.swift
@@ -1,9 +1,3 @@
-/// The foreign-content namespaces HTML5 switches into. File-scope and private.
-private enum ForeignNamespace {
- static let svg = "http://www.w3.org/2000/svg"
- static let mathml = "http://www.w3.org/1998/Math/MathML"
-}
-
/// The HTML5 tree-construction insertion modes this builder implements: the
/// document-structure subset that establishes the implied `html`, `head`, and
/// `body`. File-scope and private.
@@ -239,23 +233,6 @@ final class HTMLDocument {
node.name?.localName.lowercased() ?? ""
}
- /// Tags that close an open `` (the "in select" rule): a nested select,
- /// or an input/keygen/textarea.
- private static let closesSelect: Set = ["select", "input", "keygen", "textarea"]
-
- /// Table-structural tags that close a select which is inside a table (the "in
- /// select in table" rule).
- private static let closesSelectInTable: Set = ["caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"]
-
- /// The open-stack index of a `` that an opening table tag should close
- /// (a select inside a table), or nil when there is none.
- private func selectInTableToClose(for name: String) -> Int? {
- guard Self.closesSelectInTable.contains(name),
- let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 1,
- openBody[..= 1 {
openBody.removeLast(openBody.count - selectIndex)
@@ -288,61 +265,6 @@ final class HTMLDocument {
private func bodyInsert(_ node: PureXML.Model.TreeNode) {
openBody.last?.append(node)
}
-
- private func bodyEnsureTableContext(for name: String) {
- let structural: Set = ["table", "tbody", "thead", "tfoot", "tr"]
- guard let context = openBody.reversed().first(where: { structural.contains(tagName($0)) }).map(tagName) else { return }
- switch name {
- case "tr" where context == "table":
- bodyPushImplied("tbody")
- case "td", "th":
- if context == "table" {
- bodyPushImplied("tbody")
- }
- if context == "table" || ["tbody", "thead", "tfoot"].contains(context) {
- bodyPushImplied("tr")
- }
- default:
- break
- }
- }
-
- private func bodyPushImplied(_ name: String) {
- let element = PureXML.Model.TreeNode.element(name)
- openBody.last?.append(element)
- openBody.append(element)
- }
-
- /// Restores SVG attribute names to their canonical camel case.
- private func adjustedAttributes(_ attributes: [Attribute], namespace: String?) -> [Attribute] {
- guard namespace == ForeignNamespace.svg else { return attributes }
- return attributes.map { attribute in
- guard let adjusted = PureXML.HTML.ForeignNames.svgAttributes[attribute.name.localName.lowercased()] else { return attribute }
- return Attribute(adjusted, attribute.value)
- }
- }
-
- /// The foreign-content namespace for a body element: SVG/MathML on entry, the
- /// nearest open foreign ancestor's, or HTML (nil) inside an integration point.
- private func bodyForeignNamespace(for name: String) -> String? {
- if name == "svg" {
- return ForeignNamespace.svg
- }
- if name == "math" {
- return ForeignNamespace.mathml
- }
- for node in openBody.reversed() {
- guard let namespace = node.name?.namespaceURI else { continue }
- if Self.svgIntegrationPoints.contains(tagName(node)) {
- return nil
- }
- return namespace
- }
- return nil
- }
-
- /// The SVG HTML integration points: their content is parsed as HTML.
- private static let svgIntegrationPoints: Set = ["foreignobject", "desc", "title"]
}
extension HTMLDocument {
diff --git a/Sources/HTML/HTMLDocumentTableAndForeign.swift b/Sources/HTML/HTMLDocumentTableAndForeign.swift
new file mode 100644
index 0000000..319b311
--- /dev/null
+++ b/Sources/HTML/HTMLDocumentTableAndForeign.swift
@@ -0,0 +1,75 @@
+/// Table-context and foreign-content (SVG/MathML) helpers, split out so
+/// HTMLDocument's own body stays within the type-body length budget.
+extension HTMLDocument {
+ /// Tags that close an open `` (the "in select" rule): a nested select,
+ /// or an input/keygen/textarea.
+ static let closesSelect: Set = ["select", "input", "keygen", "textarea"]
+
+ /// Table-structural tags that close a select which is inside a table (the "in
+ /// select in table" rule).
+ private static let closesSelectInTable: Set = ["caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"]
+
+ /// The SVG HTML integration points: their content is parsed as HTML.
+ private static let svgIntegrationPoints: Set = ["foreignobject", "desc", "title"]
+
+ /// The open-stack index of a `` that an opening table tag should close
+ /// (a select inside a table), or nil when there is none.
+ func selectInTableToClose(for name: String) -> Int? {
+ guard Self.closesSelectInTable.contains(name),
+ let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 1,
+ openBody[.. [Attribute] {
+ guard namespace == ForeignNamespace.svg else { return attributes }
+ return attributes.map { attribute in
+ guard let adjusted = PureXML.HTML.ForeignNames.svgAttributes[attribute.name.localName.lowercased()] else { return attribute }
+ return Attribute(adjusted, attribute.value)
+ }
+ }
+
+ /// The foreign-content namespace for a body element: SVG/MathML on entry, the
+ /// nearest open foreign ancestor's, or HTML (nil) inside an integration point.
+ func bodyForeignNamespace(for name: String) -> String? {
+ if name == "svg" {
+ return ForeignNamespace.svg
+ }
+ if name == "math" {
+ return ForeignNamespace.mathml
+ }
+ for node in openBody.reversed() {
+ guard let namespace = node.name?.namespaceURI else { continue }
+ if Self.svgIntegrationPoints.contains(tagName(node)) {
+ return nil
+ }
+ return namespace
+ }
+ return nil
+ }
+}
diff --git a/Sources/HTML/HTMLForeignNamespace.swift b/Sources/HTML/HTMLForeignNamespace.swift
new file mode 100644
index 0000000..73aa834
--- /dev/null
+++ b/Sources/HTML/HTMLForeignNamespace.swift
@@ -0,0 +1,5 @@
+/// The foreign-content namespaces HTML5 switches into.
+enum ForeignNamespace {
+ static let svg = "http://www.w3.org/2000/svg"
+ static let mathml = "http://www.w3.org/1998/Math/MathML"
+}
diff --git a/Sources/Parsing/EventReader.swift b/Sources/Parsing/EventReader.swift
index 2584652..9483371 100644
--- a/Sources/Parsing/EventReader.swift
+++ b/Sources/Parsing/EventReader.swift
@@ -211,101 +211,6 @@ public extension PureXML.Parsing {
return .startElement(name: resolved.name, attributes: resolved.attributes)
}
- /// Returns the next text event, or nil after splicing: a reference to
- /// an entity whose replacement (directly or transitively) contains
- /// markup is validated, budgeted, and pushed back into the reader so
- /// it is reparsed as content (4.4.2 Included), elements in replacement
- /// text become elements in the stream, not character data.
- private mutating func scanText() throws -> Event? {
- let mark = reader.mark
- var raw = ""
- var length = 0
- // Whether any '&' was seen: without one the text has no references
- // and no markup-entity boundary, so the whole decode/split/findings
- // pass is skipped. The scanners visit every byte already, so this
- // costs nothing beyond a compare they were doing anyway.
- var sawAmpersand = false
- while true {
- // Bulk byte runs first: plain ASCII content (the bulk of any
- // document) skips the per-character machinery entirely; the
- // run never spans '<', CR, ']', or invalid bytes, so the
- // character path below keeps exact error marks.
- if let run = reader.contentRunBytes(sawAmpersand: &sawAmpersand) {
- length += run.utf8.count
- try checkContent(length, mark)
- raw += run
- continue
- }
- // Detect the closing '<' on the byte path so the buffer stays
- // empty: a buffered '<' would force the next element's dispatch,
- // matches(), and name scan onto the slow Character path.
- if reader.peekByte() == 0x3C {
- break
- }
- guard let character = reader.peek(), character != "<" else { break }
- length += 1
- try checkContent(length, mark)
- if character == ">", raw.hasSuffix("]]") {
- throw ParseError.cdataCloseInContent(reader.mark)
- }
- guard character.unicodeScalars.allSatisfy(XMLCharacter.isChar) else {
- throw ParseError.invalidCharacter(reader.mark)
- }
- if character == "&" {
- sawAmpersand = true
- }
- raw.append(character)
- reader.advance()
- }
- guard sawAmpersand else { return .characters(raw) }
- let split = EntityDecoder.splitAtMarkupEntity(raw, entities: referencableEntities)
- if let split {
- let replacement = try EntityDecoder.includeForContent(
- split.name,
- entities: referencableEntities,
- budget: &entityBudget,
- at: mark,
- )
- reader.inject(replacement + split.remainder)
- let prefix = try decodeReferences(split.prefix, at: mark)
- return prefix.isEmpty ? nil : .characters(prefix)
- }
- let decoded = try decodeReferences(raw, at: mark)
- recordReferenceContentFindings(raw: raw, decoded: decoded)
- return .characters(decoded)
- }
-
- mutating func scanProcessingInstruction() throws -> (target: String, data: String) {
- let mark = reader.mark
- reader.consume("")
- let target = try scanName()
- // A PI target is an NCName under namespaces (no colon).
- if target.description.contains(":") {
- throw ParseError.namespaceConstraint(reason: "PI target '\(target.description)' is not a legal NCName", mark)
- }
- // Production 16: the target and the data must be separated by
- // whitespace, so `` is not well-formed.
- if let next = reader.peek(), !next.isWhitespace, !reader.matches("?>") {
- throw ParseError.unexpectedCharacter(next, reader.mark)
- }
- reader.skipSpace()
- var data = ""
- var length = 0
- while !reader.matches("?>") {
- guard let character = reader.advance() else {
- throw ParseError.unterminatedTag(mark)
- }
- length += 1
- try checkContent(length, mark)
- guard character.unicodeScalars.allSatisfy(XMLCharacter.isChar) else {
- throw ParseError.invalidCharacter(reader.mark)
- }
- data.append(character)
- }
- reader.consume("?>")
- return (target.description, data)
- }
-
mutating func scanName() throws -> PureXML.Model.QualifiedName {
let mark = reader.mark
// Fast path: an all-ASCII name scanned directly from the bytes, with
diff --git a/Sources/Parsing/EventReaderText.swift b/Sources/Parsing/EventReaderText.swift
new file mode 100644
index 0000000..a9e9985
--- /dev/null
+++ b/Sources/Parsing/EventReaderText.swift
@@ -0,0 +1,98 @@
+/// Text and processing-instruction scanning, split from the reader body to
+/// keep it under the length caps. Behavior is part of EventReader proper.
+extension PureXML.Parsing.EventReader {
+ /// Returns the next text event, or nil after splicing: a reference to
+ /// an entity whose replacement (directly or transitively) contains
+ /// markup is validated, budgeted, and pushed back into the reader so
+ /// it is reparsed as content (4.4.2 Included), elements in replacement
+ /// text become elements in the stream, not character data.
+ mutating func scanText() throws -> Event? {
+ let mark = reader.mark
+ var raw = ""
+ var length = 0
+ // Whether any '&' was seen: without one the text has no references
+ // and no markup-entity boundary, so the whole decode/split/findings
+ // pass is skipped. The scanners visit every byte already, so this
+ // costs nothing beyond a compare they were doing anyway.
+ var sawAmpersand = false
+ while true {
+ // Bulk byte runs first: plain ASCII content (the bulk of any
+ // document) skips the per-character machinery entirely; the
+ // run never spans '<', CR, ']', or invalid bytes, so the
+ // character path below keeps exact error marks.
+ if let run = reader.contentRunBytes(sawAmpersand: &sawAmpersand) {
+ length += run.utf8.count
+ try checkContent(length, mark)
+ raw += run
+ continue
+ }
+ // Detect the closing '<' on the byte path so the buffer stays
+ // empty: a buffered '<' would force the next element's dispatch,
+ // matches(), and name scan onto the slow Character path.
+ if reader.peekByte() == 0x3C {
+ break
+ }
+ guard let character = reader.peek(), character != "<" else { break }
+ length += 1
+ try checkContent(length, mark)
+ if character == ">", raw.hasSuffix("]]") {
+ throw ParseError.cdataCloseInContent(reader.mark)
+ }
+ guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isChar) else {
+ throw ParseError.invalidCharacter(reader.mark)
+ }
+ if character == "&" {
+ sawAmpersand = true
+ }
+ raw.append(character)
+ reader.advance()
+ }
+ guard sawAmpersand else { return .characters(raw) }
+ let split = EntityDecoder.splitAtMarkupEntity(raw, entities: referencableEntities)
+ if let split {
+ let replacement = try EntityDecoder.includeForContent(
+ split.name,
+ entities: referencableEntities,
+ budget: &entityBudget,
+ at: mark,
+ )
+ reader.inject(replacement + split.remainder)
+ let prefix = try decodeReferences(split.prefix, at: mark)
+ return prefix.isEmpty ? nil : .characters(prefix)
+ }
+ let decoded = try decodeReferences(raw, at: mark)
+ recordReferenceContentFindings(raw: raw, decoded: decoded)
+ return .characters(decoded)
+ }
+
+ mutating func scanProcessingInstruction() throws -> (target: String, data: String) {
+ let mark = reader.mark
+ reader.consume("")
+ let target = try scanName()
+ // A PI target is an NCName under namespaces (no colon).
+ if target.description.contains(":") {
+ throw ParseError.namespaceConstraint(reason: "PI target '\(target.description)' is not a legal NCName", mark)
+ }
+ // Production 16: the target and the data must be separated by
+ // whitespace, so `` is not well-formed.
+ if let next = reader.peek(), !next.isWhitespace, !reader.matches("?>") {
+ throw ParseError.unexpectedCharacter(next, reader.mark)
+ }
+ reader.skipSpace()
+ var data = ""
+ var length = 0
+ while !reader.matches("?>") {
+ guard let character = reader.advance() else {
+ throw ParseError.unterminatedTag(mark)
+ }
+ length += 1
+ try checkContent(length, mark)
+ guard character.unicodeScalars.allSatisfy(PureXML.Parsing.XMLCharacter.isChar) else {
+ throw ParseError.invalidCharacter(reader.mark)
+ }
+ data.append(character)
+ }
+ reader.consume("?>")
+ return (target.description, data)
+ }
+}
diff --git a/Sources/Parsing/StreamingDecoder.swift b/Sources/Parsing/StreamingDecoder.swift
index 1d5f4eb..49f36f8 100644
--- a/Sources/Parsing/StreamingDecoder.swift
+++ b/Sources/Parsing/StreamingDecoder.swift
@@ -47,152 +47,6 @@ extension PureXML.Parsing {
}
}
- /// Streams one character from a multi-byte CJK encoding.
- private mutating func nextCJK(_ encoding: InputEncoding) -> Character? {
- switch encoding {
- case .shiftJIS: nextShiftJIS()
- case .eucJP: nextEUCJP()
- case .eucKR: nextEUCKR()
- case .gbk: nextGBK()
- case .gb18030: nextGB18030()
- case .big5: nextBig5()
- case .iso2022jp: nextISO2022JP()
- case .eucTW: nextEUCTW()
- default: nextUTF8()
- }
- }
-
- /// Streams one GBK character: ASCII, `0x80` (euro), or a lead+trail.
- private mutating func nextGBK() -> Character? {
- guard let lead = nextByte() else {
- finished = true
- return nil
- }
- if lead <= 0x7F {
- return Character(Unicode.Scalar(lead))
- }
- if lead == 0x80 {
- return "\u{20AC}"
- }
- guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
- guard let trail = nextByte() else { finished = true
- return Self.replacement
- }
- return PureXML.Parsing.ByteDecoder.GBK.twoByteScalar(lead: lead, trail: trail).map(Character.init) ?? Self.replacement
- }
-
- /// Streams one GB18030 character: ASCII, `0x80` (euro), a four-byte
- /// sequence when the lead is followed by a digit, or a two-byte GBK pair.
- private mutating func nextGB18030() -> Character? {
- guard let lead = nextByte() else {
- finished = true
- return nil
- }
- if lead <= 0x7F {
- return Character(Unicode.Scalar(lead))
- }
- if lead == 0x80 {
- return "\u{20AC}"
- }
- guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
- guard let second = nextByte() else { finished = true
- return Self.replacement
- }
- if (0x30 ... 0x39).contains(second) {
- guard let third = nextByte(), let fourth = nextByte() else { finished = true
- return Self.replacement
- }
- guard (0x81 ... 0xFE).contains(third), (0x30 ... 0x39).contains(fourth) else { return Self.replacement }
- let pointer = PureXML.Parsing.ByteDecoder.GB18030.fourBytePointer(lead, second, third, fourth)
- return PureXML.Parsing.ByteDecoder.GB18030.rangeScalar(pointer).map(Character.init) ?? Self.replacement
- }
- return PureXML.Parsing.ByteDecoder.GBK.twoByteScalar(lead: lead, trail: second).map(Character.init) ?? Self.replacement
- }
-
- /// Streams one Big5 character: ASCII, or a lead+trail through the index
- /// (a two-scalar combining sequence stays one grapheme, one `Character`).
- private mutating func nextBig5() -> Character? {
- guard let lead = nextByte() else {
- finished = true
- return nil
- }
- if lead <= 0x7F {
- return Character(Unicode.Scalar(lead))
- }
- guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
- guard let trail = nextByte() else { finished = true
- return Self.replacement
- }
- guard let sequence = PureXML.Parsing.ByteDecoder.Big5.mapping(lead: lead, trail: trail) else {
- return Self.replacement
- }
- return Character(String(String.UnicodeScalarView(sequence)))
- }
-
- /// Streams one EUC-KR character: ASCII, or a lead+trail through CP949.
- private mutating func nextEUCKR() -> Character? {
- guard let lead = nextByte() else {
- finished = true
- return nil
- }
- if lead <= 0x7F {
- return Character(Unicode.Scalar(lead))
- }
- guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
- guard let trail = nextByte() else { finished = true
- return Self.replacement
- }
- return PureXML.Parsing.ByteDecoder.EUCKR.twoByteScalar(lead: lead, trail: trail).map(Character.init) ?? Self.replacement
- }
-
- /// Streams one EUC-JP character: ASCII, `0x8E`+byte (half-width katakana),
- /// `0x8F`+two bytes (JIS X 0212), or a lead+byte through JIS X 0208.
- private mutating func nextEUCJP() -> Character? {
- guard let lead = nextByte() else {
- finished = true
- return nil
- }
- if lead <= 0x7F {
- return Character(Unicode.Scalar(lead))
- }
- if lead == 0x8E {
- guard let byte = nextByte() else { finished = true
- return Self.replacement
- }
- guard (0xA1 ... 0xDF).contains(byte) else { return Self.replacement }
- return Character(Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1) ?? "\u{FFFD}")
- }
- if lead == 0x8F {
- guard let mid = nextByte(), let trail = nextByte() else { finished = true
- return Self.replacement
- }
- return PureXML.Parsing.ByteDecoder.EUCJP.twoByteScalar(lead: mid, trail: trail, useJis0212: true).map(Character.init) ?? Self.replacement
- }
- guard let trail = nextByte() else { finished = true
- return Self.replacement
- }
- return PureXML.Parsing.ByteDecoder.EUCJP.twoByteScalar(lead: lead, trail: trail, useJis0212: false).map(Character.init) ?? Self.replacement
- }
-
- /// Streams one Shift-JIS character: a single byte (ASCII or half-width
- /// katakana) or a lead+trail two-byte sequence resolved through JIS X 0208.
- private mutating func nextShiftJIS() -> Character? {
- guard let lead = nextByte() else {
- finished = true
- return nil
- }
- if let single = PureXML.Parsing.ByteDecoder.ShiftJIS.singleByteScalar(lead) {
- return Character(single)
- }
- guard PureXML.Parsing.ByteDecoder.ShiftJIS.isLead(lead) else { return Self.replacement }
- guard let trail = nextByte() else {
- finished = true
- return Self.replacement
- }
- guard let scalar = PureXML.Parsing.ByteDecoder.ShiftJIS.twoByteScalar(lead: lead, trail: trail) else { return Self.replacement }
- return Character(scalar)
- }
-
private mutating func nextUTF32(bigEndian: Bool) -> Character? {
var ordered: [UInt32] = []
for _ in 0 ..< 4 {
@@ -247,7 +101,7 @@ extension PureXML.Parsing {
return pull()
}
- private mutating func nextUTF8() -> Character? {
+ mutating func nextUTF8() -> Character? {
guard let first = nextByte() else {
finished = true
return nil
diff --git a/Sources/Parsing/StreamingDecoderCJK.swift b/Sources/Parsing/StreamingDecoderCJK.swift
new file mode 100644
index 0000000..410907f
--- /dev/null
+++ b/Sources/Parsing/StreamingDecoderCJK.swift
@@ -0,0 +1,147 @@
+extension PureXML.Parsing.StreamingDecoder {
+ /// Streams one character from a multi-byte CJK encoding.
+ mutating func nextCJK(_ encoding: PureXML.Parsing.InputEncoding) -> Character? {
+ switch encoding {
+ case .shiftJIS: nextShiftJIS()
+ case .eucJP: nextEUCJP()
+ case .eucKR: nextEUCKR()
+ case .gbk: nextGBK()
+ case .gb18030: nextGB18030()
+ case .big5: nextBig5()
+ case .iso2022jp: nextISO2022JP()
+ case .eucTW: nextEUCTW()
+ default: nextUTF8()
+ }
+ }
+
+ /// Streams one GBK character: ASCII, `0x80` (euro), or a lead+trail.
+ private mutating func nextGBK() -> Character? {
+ guard let lead = nextByte() else {
+ finished = true
+ return nil
+ }
+ if lead <= 0x7F {
+ return Character(Unicode.Scalar(lead))
+ }
+ if lead == 0x80 {
+ return "\u{20AC}"
+ }
+ guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
+ guard let trail = nextByte() else { finished = true
+ return Self.replacement
+ }
+ return PureXML.Parsing.ByteDecoder.GBK.twoByteScalar(lead: lead, trail: trail).map(Character.init) ?? Self.replacement
+ }
+
+ /// Streams one GB18030 character: ASCII, `0x80` (euro), a four-byte
+ /// sequence when the lead is followed by a digit, or a two-byte GBK pair.
+ private mutating func nextGB18030() -> Character? {
+ guard let lead = nextByte() else {
+ finished = true
+ return nil
+ }
+ if lead <= 0x7F {
+ return Character(Unicode.Scalar(lead))
+ }
+ if lead == 0x80 {
+ return "\u{20AC}"
+ }
+ guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
+ guard let second = nextByte() else { finished = true
+ return Self.replacement
+ }
+ if (0x30 ... 0x39).contains(second) {
+ guard let third = nextByte(), let fourth = nextByte() else { finished = true
+ return Self.replacement
+ }
+ guard (0x81 ... 0xFE).contains(third), (0x30 ... 0x39).contains(fourth) else { return Self.replacement }
+ let pointer = PureXML.Parsing.ByteDecoder.GB18030.fourBytePointer(lead, second, third, fourth)
+ return PureXML.Parsing.ByteDecoder.GB18030.rangeScalar(pointer).map(Character.init) ?? Self.replacement
+ }
+ return PureXML.Parsing.ByteDecoder.GBK.twoByteScalar(lead: lead, trail: second).map(Character.init) ?? Self.replacement
+ }
+
+ /// Streams one Big5 character: ASCII, or a lead+trail through the index
+ /// (a two-scalar combining sequence stays one grapheme, one `Character`).
+ private mutating func nextBig5() -> Character? {
+ guard let lead = nextByte() else {
+ finished = true
+ return nil
+ }
+ if lead <= 0x7F {
+ return Character(Unicode.Scalar(lead))
+ }
+ guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
+ guard let trail = nextByte() else { finished = true
+ return Self.replacement
+ }
+ guard let sequence = PureXML.Parsing.ByteDecoder.Big5.mapping(lead: lead, trail: trail) else {
+ return Self.replacement
+ }
+ return Character(String(String.UnicodeScalarView(sequence)))
+ }
+
+ /// Streams one EUC-KR character: ASCII, or a lead+trail through CP949.
+ private mutating func nextEUCKR() -> Character? {
+ guard let lead = nextByte() else {
+ finished = true
+ return nil
+ }
+ if lead <= 0x7F {
+ return Character(Unicode.Scalar(lead))
+ }
+ guard (0x81 ... 0xFE).contains(lead) else { return Self.replacement }
+ guard let trail = nextByte() else { finished = true
+ return Self.replacement
+ }
+ return PureXML.Parsing.ByteDecoder.EUCKR.twoByteScalar(lead: lead, trail: trail).map(Character.init) ?? Self.replacement
+ }
+
+ /// Streams one EUC-JP character: ASCII, `0x8E`+byte (half-width katakana),
+ /// `0x8F`+two bytes (JIS X 0212), or a lead+byte through JIS X 0208.
+ private mutating func nextEUCJP() -> Character? {
+ guard let lead = nextByte() else {
+ finished = true
+ return nil
+ }
+ if lead <= 0x7F {
+ return Character(Unicode.Scalar(lead))
+ }
+ if lead == 0x8E {
+ guard let byte = nextByte() else { finished = true
+ return Self.replacement
+ }
+ guard (0xA1 ... 0xDF).contains(byte) else { return Self.replacement }
+ return Character(Unicode.Scalar(0xFF61 + UInt32(byte) - 0xA1) ?? "\u{FFFD}")
+ }
+ if lead == 0x8F {
+ guard let mid = nextByte(), let trail = nextByte() else { finished = true
+ return Self.replacement
+ }
+ return PureXML.Parsing.ByteDecoder.EUCJP.twoByteScalar(lead: mid, trail: trail, useJis0212: true).map(Character.init) ?? Self.replacement
+ }
+ guard let trail = nextByte() else { finished = true
+ return Self.replacement
+ }
+ return PureXML.Parsing.ByteDecoder.EUCJP.twoByteScalar(lead: lead, trail: trail, useJis0212: false).map(Character.init) ?? Self.replacement
+ }
+
+ /// Streams one Shift-JIS character: a single byte (ASCII or half-width
+ /// katakana) or a lead+trail two-byte sequence resolved through JIS X 0208.
+ private mutating func nextShiftJIS() -> Character? {
+ guard let lead = nextByte() else {
+ finished = true
+ return nil
+ }
+ if let single = PureXML.Parsing.ByteDecoder.ShiftJIS.singleByteScalar(lead) {
+ return Character(single)
+ }
+ guard PureXML.Parsing.ByteDecoder.ShiftJIS.isLead(lead) else { return Self.replacement }
+ guard let trail = nextByte() else {
+ finished = true
+ return Self.replacement
+ }
+ guard let scalar = PureXML.Parsing.ByteDecoder.ShiftJIS.twoByteScalar(lead: lead, trail: trail) else { return Self.replacement }
+ return Character(scalar)
+ }
+}
diff --git a/Sources/Schema/RelaxNGDerivative.swift b/Sources/Schema/RelaxNGDerivative.swift
index 30d39b2..2aed0bf 100644
--- a/Sources/Schema/RelaxNGDerivative.swift
+++ b/Sources/Schema/RelaxNGDerivative.swift
@@ -284,7 +284,7 @@ extension PureXML.Schema.RelaxNGEngine {
}
}
- private func endTagDeriv(_ pattern: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
+ func endTagDeriv(_ pattern: PureXML.Schema.Pattern) -> PureXML.Schema.Pattern {
switch pattern {
case let .choice(lhs, rhs): choice(endTagDeriv(lhs), endTagDeriv(rhs))
case let .after(lhs, rhs): nullable(lhs) ? rhs : .notAllowed
@@ -319,7 +319,7 @@ extension PureXML.Schema.RelaxNGEngine {
return result
}
- private static func isWhitespace(_ string: String) -> Bool {
+ static func isWhitespace(_ string: String) -> Bool {
string.allSatisfy(\.isWhitespace)
}
@@ -342,92 +342,3 @@ extension PureXML.Schema.RelaxNGEngine {
return counts
}
}
-
-/// One open element while streaming a RELAX NG derivative: whether it has had an
-/// element child yet, and the text buffered since the last structural event. The
-/// `after`-threaded pattern is the validation stack; this frame only carries the
-/// bookkeeping `childrenDeriv` needs to treat ignorable whitespace correctly.
-private struct RelaxNGStreamFrame {
- var hadElementChild = false
- var pendingText = ""
- /// The namespace bindings in scope at this element, for QName values.
- var env: [String: String] = [:]
-}
-
-public extension PureXML.Schema {
- /// The residual derivative plus per-element whitespace bookkeeping carried
- /// across the events of a streamed RELAX NG validation.
- struct RelaxNGStreamingState {
- var current: PureXML.Schema.Pattern
- fileprivate var frames: [RelaxNGStreamFrame]
- }
-}
-
-extension PureXML.Schema.RelaxNGEngine {
- /// The initial streaming state for a start pattern, with a document-level frame.
- func streamingStart(_ start: PureXML.Schema.Pattern) -> PureXML.Schema.RelaxNGStreamingState {
- PureXML.Schema.RelaxNGStreamingState(current: start, frames: [RelaxNGStreamFrame(env: Self.rootEnvironment)])
- }
-
- /// Derives the state by one parse event, the streaming form of `childDeriv`.
- /// Text is buffered and applied at structural boundaries so ignorable
- /// whitespace is treated exactly as the tree walk's `childrenDeriv` does.
- func streamingConsume(_ event: PureXML.Parsing.Event, into state: inout PureXML.Schema.RelaxNGStreamingState) {
- switch event {
- case let .startElement(name, attributes):
- flushText(&state, closing: false)
- if !state.frames.isEmpty {
- state.frames[state.frames.count - 1].hadElementChild = true
- }
- var env = state.frames.last?.env ?? Self.rootEnvironment
- for attribute in attributes where Self.isNamespaceDeclaration(attribute) {
- env[attribute.name.prefix == "xmlns" ? attribute.name.localName : ""] = attribute.value
- }
- let opened = startTagOpenDeriv(state.current, name)
- let attributed = attributes
- .filter { !Self.isNamespaceDeclaration($0) }
- .reduce(opened) { attributeDeriv($0, $1, env: env) }
- state.current = startTagCloseDeriv(attributed)
- state.frames.append(RelaxNGStreamFrame(env: env))
- case let .characters(text), let .cdata(text):
- if !state.frames.isEmpty {
- state.frames[state.frames.count - 1].pendingText += text
- }
- case .endElement:
- flushText(&state, closing: true)
- state.current = endTagDeriv(state.current)
- if !state.frames.isEmpty {
- state.frames.removeLast()
- }
- case .comment, .processingInstruction:
- break
- }
- }
-
- /// Whether the streamed document satisfied the schema (the residual is nullable).
- func streamingValid(_ state: PureXML.Schema.RelaxNGStreamingState) -> Bool {
- nullable(state.current)
- }
-
- private func flushText(_ state: inout PureXML.Schema.RelaxNGStreamingState, closing: Bool) {
- guard let index = state.frames.indices.last else { return }
- let text = state.frames[index].pendingText
- let env = state.frames[index].env
- guard !text.isEmpty else {
- // An empty element is optional empty text (the tree walk's rule),
- // so a data/value pattern admitting "" matches .
- if closing, !state.frames[index].hadElementChild {
- state.current = choice(state.current, textDeriv(state.current, "", env: env))
- }
- return
- }
- state.frames[index].pendingText = ""
- if !Self.isWhitespace(text) {
- state.current = textDeriv(state.current, text, env: env)
- } else if closing, !state.frames[index].hadElementChild {
- // A lone whitespace text child is optional, the `childrenDeriv` rule.
- state.current = choice(state.current, textDeriv(state.current, text, env: env))
- }
- // Whitespace among element siblings is ignorable and skipped.
- }
-}
diff --git a/Sources/Schema/RelaxNGStreaming.swift b/Sources/Schema/RelaxNGStreaming.swift
new file mode 100644
index 0000000..f413038
--- /dev/null
+++ b/Sources/Schema/RelaxNGStreaming.swift
@@ -0,0 +1,91 @@
+// Streaming RELAX NG validation, split out so RelaxNGDerivative.swift stays
+// within the file length budget.
+
+/// One open element while streaming a RELAX NG derivative: whether it has had an
+/// element child yet, and the text buffered since the last structural event. The
+/// `after`-threaded pattern is the validation stack; this frame only carries the
+/// bookkeeping `childrenDeriv` needs to treat ignorable whitespace correctly.
+private struct RelaxNGStreamFrame {
+ var hadElementChild = false
+ var pendingText = ""
+ /// The namespace bindings in scope at this element, for QName values.
+ var env: [String: String] = [:]
+}
+
+public extension PureXML.Schema {
+ /// The residual derivative plus per-element whitespace bookkeeping carried
+ /// across the events of a streamed RELAX NG validation.
+ struct RelaxNGStreamingState {
+ var current: PureXML.Schema.Pattern
+ fileprivate var frames: [RelaxNGStreamFrame]
+ }
+}
+
+extension PureXML.Schema.RelaxNGEngine {
+ /// The initial streaming state for a start pattern, with a document-level frame.
+ func streamingStart(_ start: PureXML.Schema.Pattern) -> PureXML.Schema.RelaxNGStreamingState {
+ PureXML.Schema.RelaxNGStreamingState(current: start, frames: [RelaxNGStreamFrame(env: Self.rootEnvironment)])
+ }
+
+ /// Derives the state by one parse event, the streaming form of `childDeriv`.
+ /// Text is buffered and applied at structural boundaries so ignorable
+ /// whitespace is treated exactly as the tree walk's `childrenDeriv` does.
+ func streamingConsume(_ event: PureXML.Parsing.Event, into state: inout PureXML.Schema.RelaxNGStreamingState) {
+ switch event {
+ case let .startElement(name, attributes):
+ flushText(&state, closing: false)
+ if !state.frames.isEmpty {
+ state.frames[state.frames.count - 1].hadElementChild = true
+ }
+ var env = state.frames.last?.env ?? Self.rootEnvironment
+ for attribute in attributes where Self.isNamespaceDeclaration(attribute) {
+ env[attribute.name.prefix == "xmlns" ? attribute.name.localName : ""] = attribute.value
+ }
+ let opened = startTagOpenDeriv(state.current, name)
+ let attributed = attributes
+ .filter { !Self.isNamespaceDeclaration($0) }
+ .reduce(opened) { attributeDeriv($0, $1, env: env) }
+ state.current = startTagCloseDeriv(attributed)
+ state.frames.append(RelaxNGStreamFrame(env: env))
+ case let .characters(text), let .cdata(text):
+ if !state.frames.isEmpty {
+ state.frames[state.frames.count - 1].pendingText += text
+ }
+ case .endElement:
+ flushText(&state, closing: true)
+ state.current = endTagDeriv(state.current)
+ if !state.frames.isEmpty {
+ state.frames.removeLast()
+ }
+ case .comment, .processingInstruction:
+ break
+ }
+ }
+
+ /// Whether the streamed document satisfied the schema (the residual is nullable).
+ func streamingValid(_ state: PureXML.Schema.RelaxNGStreamingState) -> Bool {
+ nullable(state.current)
+ }
+
+ private func flushText(_ state: inout PureXML.Schema.RelaxNGStreamingState, closing: Bool) {
+ guard let index = state.frames.indices.last else { return }
+ let text = state.frames[index].pendingText
+ let env = state.frames[index].env
+ guard !text.isEmpty else {
+ // An empty element is optional empty text (the tree walk's rule),
+ // so a data/value pattern admitting "" matches .
+ if closing, !state.frames[index].hadElementChild {
+ state.current = choice(state.current, textDeriv(state.current, "", env: env))
+ }
+ return
+ }
+ state.frames[index].pendingText = ""
+ if !Self.isWhitespace(text) {
+ state.current = textDeriv(state.current, text, env: env)
+ } else if closing, !state.frames[index].hadElementChild {
+ // A lone whitespace text child is optional, the `childrenDeriv` rule.
+ state.current = choice(state.current, textDeriv(state.current, text, env: env))
+ }
+ // Whitespace among element siblings is ignorable and skipped.
+ }
+}
diff --git a/Sources/Schema/SchemaReferences.swift b/Sources/Schema/SchemaReferences.swift
index 7eb9196..e36d48f 100644
--- a/Sources/Schema/SchemaReferences.swift
+++ b/Sources/Schema/SchemaReferences.swift
@@ -232,176 +232,4 @@ extension PureXML.Schema.XSDParser {
}
return findings
}
-
- static func simpleTypeBaseNotComplexFindings(_ schema: XSDTree, in context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
- var findings: [PureXML.Schema.SchemaLocatedFinding] = []
- let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
- let targetNamespace = context.targetNamespace
-
- func isComplex(_ qname: String) -> Bool {
- let trimmed = qname.trimmingXMLWhitespace()
- let prefix = PureXML.Schema.XSDNode.prefix(trimmed)
- let uri = prefix.flatMap { bindings[$0] } ?? bindings[""]
- let local = PureXML.Schema.XSDNode.stripPrefix(trimmed)
- if uri == xsdNamespace {
- if targetNamespace == xsdNamespace {
- return local == "anyType" || context.complexTypeNodes[local] != nil
- } else {
- return local == "anyType"
- }
- }
- if uri == targetNamespace || uri == nil || uri == "" {
- return context.complexTypeNodes[local] != nil
- }
- return false
- }
-
- func check(_ node: XSDTree) {
- let local = PureXML.Schema.XSDNode.localName(node)
- if local == "appinfo" || local == "documentation" {
- return
- }
-
- if node.name?.namespaceURI == xsdNamespace {
- switch local {
- case "restriction":
- Self.checkRestrictionUnderSimpleType(node, isComplex: isComplex, findings: &findings)
- case "list":
- Self.checkList(node, isComplex: isComplex, findings: &findings)
- case "union":
- Self.checkUnion(node, isComplex: isComplex, findings: &findings)
- default:
- break
- }
- }
-
- for child in PureXML.Schema.XSDNode.elementChildren(node) {
- check(child)
- }
- }
- check(schema)
- return findings
- }
-
- private static func checkRestrictionUnderSimpleType(_ node: XSDTree, isComplex: (String) -> Bool, findings: inout [PureXML.Schema.SchemaLocatedFinding]) {
- guard let parent = node.parent, parent.name?.namespaceURI == xsdNamespace, PureXML.Schema.XSDNode.localName(parent) == "simpleType" else { return }
- if let base = PureXML.Schema.XSDNode.attribute(node, "base"), isComplex(base) {
- findings.append(PureXML.Schema.SchemaLocatedFinding(
- reason: "base type '\(base)' of simpleType restriction must be a simple type, not a complex type",
- node: node,
- ))
- }
- }
-
- private static func checkList(_ node: XSDTree, isComplex: (String) -> Bool, findings: inout [PureXML.Schema.SchemaLocatedFinding]) {
- if let itemType = PureXML.Schema.XSDNode.attribute(node, "itemType"), isComplex(itemType) {
- findings.append(PureXML.Schema.SchemaLocatedFinding(
- reason: "itemType '\(itemType)' of list must be a simple type, not a complex type",
- node: node,
- ))
- }
- }
-
- private static func checkUnion(_ node: XSDTree, isComplex: (String) -> Bool, findings: inout [PureXML.Schema.SchemaLocatedFinding]) {
- if let members = PureXML.Schema.XSDNode.attribute(node, "memberTypes") {
- for token in members.split(whereSeparator: \.isWhitespace) where isComplex(String(token)) {
- findings.append(PureXML.Schema.SchemaLocatedFinding(
- reason: "memberType '\(token)' of union must be a simple type, not a complex type",
- node: node,
- ))
- }
- }
- }
-
- private static func xsdNamespaceReferenceFindings(_ schema: XSDTree) -> [PureXML.Schema.SchemaLocatedFinding] {
- let targetNamespace = PureXML.Schema.XSDNode.attribute(schema, "targetNamespace")
- if targetNamespace == xsdNamespace {
- return []
- }
- var findings: [PureXML.Schema.SchemaLocatedFinding] = []
- let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
-
- func isUndeclaredBuiltin(_ qname: String) -> Bool {
- let prefix = PureXML.Schema.XSDNode.prefix(qname)
- let uri = prefix.flatMap { bindings[$0] } ?? bindings[""]
- if uri == xsdNamespace {
- let localName = PureXML.Schema.XSDNode.stripPrefix(qname)
- return !referenceBuiltins.contains(localName)
- }
- return false
- }
-
- func check(_ node: XSDTree) {
- let local = PureXML.Schema.XSDNode.localName(node)
- if local == "appinfo" || local == "documentation" {
- return
- }
-
- if node.name?.namespaceURI == xsdNamespace {
- for attribute in ["type", "base", "itemType"] {
- if let qname = PureXML.Schema.XSDNode.attribute(node, attribute), isUndeclaredBuiltin(qname) {
- findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "\(attribute) references undeclared type '\(qname)'", node: node))
- }
- }
- if let members = PureXML.Schema.XSDNode.attribute(node, "memberTypes") {
- for token in members.split(whereSeparator: \.isWhitespace) {
- let qname = String(token)
- if isUndeclaredBuiltin(qname) {
- findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "memberTypes references undeclared type '\(qname)'", node: node))
- }
- }
- }
- }
-
- for child in PureXML.Schema.XSDNode.elementChildren(node) {
- check(child)
- }
- }
- check(schema)
- return findings
- }
-
- private static func simpleContentBaseFindings(_ schema: XSDTree, in context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
- var findings: [PureXML.Schema.SchemaLocatedFinding] = []
- let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
- let targetNamespace = context.targetNamespace
-
- func check(_ node: XSDTree) {
- let local = PureXML.Schema.XSDNode.localName(node)
- if local == "appinfo" || local == "documentation" {
- return
- }
-
- let isSimpleContentRestriction = node.name?.namespaceURI == xsdNamespace && local == "restriction"
- && node.parent?.name?.namespaceURI == xsdNamespace
- && node.parent.flatMap { PureXML.Schema.XSDNode.localName($0) } == "simpleContent"
- if isSimpleContentRestriction {
- if let base = PureXML.Schema.XSDNode.attribute(node, "base") {
- let prefix = PureXML.Schema.XSDNode.prefix(base)
- let uri = prefix.flatMap { bindings[$0] } ?? bindings[""]
- let localPart = PureXML.Schema.XSDNode.stripPrefix(base)
- if uri == targetNamespace || uri == nil || uri == "" {
- if let baseNode = context.complexTypeNodes[localPart] {
- let children = PureXML.Schema.XSDNode.elementChildren(baseNode)
- let hasSimpleContent = children.contains { child in
- child.name?.namespaceURI == xsdNamespace && PureXML.Schema.XSDNode.localName(child) == "simpleContent"
- }
- if !hasSimpleContent {
- findings.append(PureXML.Schema.SchemaLocatedFinding(
- reason: "base type '\(base)' of simpleContent restriction must be a simple type or complex type with simpleContent",
- node: node,
- ))
- }
- }
- }
- }
- }
-
- for child in PureXML.Schema.XSDNode.elementChildren(node) {
- check(child)
- }
- }
- check(schema)
- return findings
- }
}
diff --git a/Sources/Schema/SchemaSimpleContentCompatibility.swift b/Sources/Schema/SchemaSimpleContentCompatibility.swift
new file mode 100644
index 0000000..94c6c8c
--- /dev/null
+++ b/Sources/Schema/SchemaSimpleContentCompatibility.swift
@@ -0,0 +1,176 @@
+/// Type-compatibility findings for simpleType derivations and simpleContent
+/// restrictions, split out so SchemaReferences.swift stays within the file
+/// length budget.
+extension PureXML.Schema.XSDParser {
+ static func simpleTypeBaseNotComplexFindings(_ schema: XSDTree, in context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
+ var findings: [PureXML.Schema.SchemaLocatedFinding] = []
+ let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
+ let targetNamespace = context.targetNamespace
+
+ func isComplex(_ qname: String) -> Bool {
+ let trimmed = qname.trimmingXMLWhitespace()
+ let prefix = PureXML.Schema.XSDNode.prefix(trimmed)
+ let uri = prefix.flatMap { bindings[$0] } ?? bindings[""]
+ let local = PureXML.Schema.XSDNode.stripPrefix(trimmed)
+ if uri == xsdNamespace {
+ if targetNamespace == xsdNamespace {
+ return local == "anyType" || context.complexTypeNodes[local] != nil
+ } else {
+ return local == "anyType"
+ }
+ }
+ if uri == targetNamespace || uri == nil || uri == "" {
+ return context.complexTypeNodes[local] != nil
+ }
+ return false
+ }
+
+ func check(_ node: XSDTree) {
+ let local = PureXML.Schema.XSDNode.localName(node)
+ if local == "appinfo" || local == "documentation" {
+ return
+ }
+
+ if node.name?.namespaceURI == xsdNamespace {
+ switch local {
+ case "restriction":
+ Self.checkRestrictionUnderSimpleType(node, isComplex: isComplex, findings: &findings)
+ case "list":
+ Self.checkList(node, isComplex: isComplex, findings: &findings)
+ case "union":
+ Self.checkUnion(node, isComplex: isComplex, findings: &findings)
+ default:
+ break
+ }
+ }
+
+ for child in PureXML.Schema.XSDNode.elementChildren(node) {
+ check(child)
+ }
+ }
+ check(schema)
+ return findings
+ }
+
+ private static func checkRestrictionUnderSimpleType(_ node: XSDTree, isComplex: (String) -> Bool, findings: inout [PureXML.Schema.SchemaLocatedFinding]) {
+ guard let parent = node.parent, parent.name?.namespaceURI == xsdNamespace, PureXML.Schema.XSDNode.localName(parent) == "simpleType" else { return }
+ if let base = PureXML.Schema.XSDNode.attribute(node, "base"), isComplex(base) {
+ findings.append(PureXML.Schema.SchemaLocatedFinding(
+ reason: "base type '\(base)' of simpleType restriction must be a simple type, not a complex type",
+ node: node,
+ ))
+ }
+ }
+
+ private static func checkList(_ node: XSDTree, isComplex: (String) -> Bool, findings: inout [PureXML.Schema.SchemaLocatedFinding]) {
+ if let itemType = PureXML.Schema.XSDNode.attribute(node, "itemType"), isComplex(itemType) {
+ findings.append(PureXML.Schema.SchemaLocatedFinding(
+ reason: "itemType '\(itemType)' of list must be a simple type, not a complex type",
+ node: node,
+ ))
+ }
+ }
+
+ private static func checkUnion(_ node: XSDTree, isComplex: (String) -> Bool, findings: inout [PureXML.Schema.SchemaLocatedFinding]) {
+ if let members = PureXML.Schema.XSDNode.attribute(node, "memberTypes") {
+ for token in members.split(whereSeparator: \.isWhitespace) where isComplex(String(token)) {
+ findings.append(PureXML.Schema.SchemaLocatedFinding(
+ reason: "memberType '\(token)' of union must be a simple type, not a complex type",
+ node: node,
+ ))
+ }
+ }
+ }
+
+ static func xsdNamespaceReferenceFindings(_ schema: XSDTree) -> [PureXML.Schema.SchemaLocatedFinding] {
+ let targetNamespace = PureXML.Schema.XSDNode.attribute(schema, "targetNamespace")
+ if targetNamespace == xsdNamespace {
+ return []
+ }
+ var findings: [PureXML.Schema.SchemaLocatedFinding] = []
+ let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
+
+ func isUndeclaredBuiltin(_ qname: String) -> Bool {
+ let prefix = PureXML.Schema.XSDNode.prefix(qname)
+ let uri = prefix.flatMap { bindings[$0] } ?? bindings[""]
+ if uri == xsdNamespace {
+ let localName = PureXML.Schema.XSDNode.stripPrefix(qname)
+ return !referenceBuiltins.contains(localName)
+ }
+ return false
+ }
+
+ func check(_ node: XSDTree) {
+ let local = PureXML.Schema.XSDNode.localName(node)
+ if local == "appinfo" || local == "documentation" {
+ return
+ }
+
+ if node.name?.namespaceURI == xsdNamespace {
+ for attribute in ["type", "base", "itemType"] {
+ if let qname = PureXML.Schema.XSDNode.attribute(node, attribute), isUndeclaredBuiltin(qname) {
+ findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "\(attribute) references undeclared type '\(qname)'", node: node))
+ }
+ }
+ if let members = PureXML.Schema.XSDNode.attribute(node, "memberTypes") {
+ for token in members.split(whereSeparator: \.isWhitespace) {
+ let qname = String(token)
+ if isUndeclaredBuiltin(qname) {
+ findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "memberTypes references undeclared type '\(qname)'", node: node))
+ }
+ }
+ }
+ }
+
+ for child in PureXML.Schema.XSDNode.elementChildren(node) {
+ check(child)
+ }
+ }
+ check(schema)
+ return findings
+ }
+
+ static func simpleContentBaseFindings(_ schema: XSDTree, in context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
+ var findings: [PureXML.Schema.SchemaLocatedFinding] = []
+ let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
+ let targetNamespace = context.targetNamespace
+
+ func check(_ node: XSDTree) {
+ let local = PureXML.Schema.XSDNode.localName(node)
+ if local == "appinfo" || local == "documentation" {
+ return
+ }
+
+ let isSimpleContentRestriction = node.name?.namespaceURI == xsdNamespace && local == "restriction"
+ && node.parent?.name?.namespaceURI == xsdNamespace
+ && node.parent.flatMap { PureXML.Schema.XSDNode.localName($0) } == "simpleContent"
+ if isSimpleContentRestriction {
+ if let base = PureXML.Schema.XSDNode.attribute(node, "base") {
+ let prefix = PureXML.Schema.XSDNode.prefix(base)
+ let uri = prefix.flatMap { bindings[$0] } ?? bindings[""]
+ let localPart = PureXML.Schema.XSDNode.stripPrefix(base)
+ if uri == targetNamespace || uri == nil || uri == "" {
+ if let baseNode = context.complexTypeNodes[localPart] {
+ let children = PureXML.Schema.XSDNode.elementChildren(baseNode)
+ let hasSimpleContent = children.contains { child in
+ child.name?.namespaceURI == xsdNamespace && PureXML.Schema.XSDNode.localName(child) == "simpleContent"
+ }
+ if !hasSimpleContent {
+ findings.append(PureXML.Schema.SchemaLocatedFinding(
+ reason: "base type '\(base)' of simpleContent restriction must be a simple type or complex type with simpleContent",
+ node: node,
+ ))
+ }
+ }
+ }
+ }
+ }
+
+ for child in PureXML.Schema.XSDNode.elementChildren(node) {
+ check(child)
+ }
+ }
+ check(schema)
+ return findings
+ }
+}
diff --git a/Sources/Schema/SchemaSimpleTypeFacetChecks.swift b/Sources/Schema/SchemaSimpleTypeFacetChecks.swift
new file mode 100644
index 0000000..c11ed9a
--- /dev/null
+++ b/Sources/Schema/SchemaSimpleTypeFacetChecks.swift
@@ -0,0 +1,137 @@
+/// The per-facet validation checks, split out so SimpleType's own body stays
+/// within the type-body length budget.
+extension PureXML.Schema.SimpleType {
+ private func boundsError(_ value: String, _: PureXML.Schema.Primitive) -> String? {
+ let (lower, upper) = base.bounds
+ guard lower != nil || upper != nil, let decimal = PureXML.Schema.DecimalValue(value, allowFraction: false) else {
+ return nil
+ }
+ if let lower, decimal < lower {
+ return "\(value) is below the minimum for \(base.rawValue)"
+ }
+ if let upper, decimal > upper {
+ return "\(value) is above the maximum for \(base.rawValue)"
+ }
+ return nil
+ }
+
+ private func lengthError(_ value: String, _ primitive: PureXML.Schema.Primitive) -> String? {
+ // length/minLength/maxLength measure characters, octets, or list
+ // items depending on the type; for QName the spec leaves the unit
+ // unspecified (XSD 1.0 Datatypes 4.3.1), so like Xerces and the XSTS
+ // NIST oracle we do not constrain QName values by length.
+ if case .qName = primitive {
+ return nil
+ }
+ if case .notation = primitive {
+ return nil
+ }
+ let length = primitive.measuredLength(value)
+ if let exact = facets.length, length != exact {
+ return "length \(length) is not \(exact)"
+ }
+ if let minimum = facets.minLength, length < minimum {
+ return "length \(length) is below \(minimum)"
+ }
+ if let maximum = facets.maxLength, length > maximum {
+ return "length \(length) is above \(maximum)"
+ }
+ return nil
+ }
+
+ private func digitsError(_ value: String) -> String? {
+ guard facets.totalDigits != nil || facets.fractionDigits != nil,
+ let decimal = PureXML.Schema.DecimalValue(value, allowFraction: true) else { return nil }
+ if let total = facets.totalDigits, decimal.totalDigits > total {
+ return "more than \(total) total digits"
+ }
+ if let fraction = facets.fractionDigits, decimal.fractionDigits.count > fraction {
+ return "more than \(fraction) fraction digits"
+ }
+ return nil
+ }
+
+ func patternError(_ value: String) -> String? {
+ for group in facets.patternGroups {
+ let matched = group.contains { pattern in
+ guard let regex = try? PureXML.Regex.Pattern(pattern) else { return false }
+ return regex.matches(value)
+ }
+ guard matched else {
+ let label = group.joined(separator: "|")
+ return "'\(value)' does not match pattern '\(label)'"
+ }
+ }
+ return nil
+ }
+
+ private func enumerationError(_ value: String, _ primitive: PureXML.Schema.Primitive) -> String? {
+ guard let enumeration = facets.enumeration else { return nil }
+ let whiteSpace = facets.whiteSpace ?? base.whiteSpace
+ let matched = enumeration.contains { candidate in
+ let normalized = Self.process(candidate, whiteSpace: whiteSpace)
+ if let lhs = primitive.ordered(value), let rhs = primitive.ordered(normalized) {
+ return lhs == rhs
+ }
+ return normalized == value
+ }
+ return matched ? nil : "'\(value)' is not in the enumeration"
+ }
+
+ private func rangeError(_ value: String, _ primitive: PureXML.Schema.Primitive) -> String? {
+ if case .duration = primitive {
+ return durationRangeError(value)
+ }
+ guard let actual = primitive.ordered(value) else { return nil }
+ if let bound = facets.minInclusive, let limit = primitive.ordered(bound), actual < limit {
+ return "\(value) is below the inclusive minimum"
+ }
+ if let bound = facets.maxInclusive, let limit = primitive.ordered(bound), actual > limit {
+ return "\(value) is above the inclusive maximum"
+ }
+ if let bound = facets.minExclusive, let limit = primitive.ordered(bound), !(limit < actual) {
+ return "\(value) is not above the exclusive minimum"
+ }
+ if let bound = facets.maxExclusive, let limit = primitive.ordered(bound), !(actual < limit) {
+ return "\(value) is not below the exclusive maximum"
+ }
+ return nil
+ }
+
+ /// Ordering facets on `xs:duration`, which is a partial order: a value
+ /// that is incomparable to the bound does not satisfy it (it is not in
+ /// the ordered range), so an incomparable result is a violation.
+ private func durationRangeError(_ value: String) -> String? {
+ guard let actual = PureXML.Schema.DurationValue(value) else { return nil }
+ func order(_ bound: String?) -> PureXML.Schema.DurationOrder? {
+ bound.flatMap(PureXML.Schema.DurationValue.init).map(actual.compare(to:))
+ }
+ if let order = order(facets.minInclusive), order == .lessThan || order == .incomparable {
+ return "\(value) is below the inclusive minimum"
+ }
+ if let order = order(facets.maxInclusive), order == .greaterThan || order == .incomparable {
+ return "\(value) is above the inclusive maximum"
+ }
+ if let order = order(facets.minExclusive), order != .greaterThan {
+ return "\(value) is not above the exclusive minimum"
+ }
+ if let order = order(facets.maxExclusive), order != .lessThan {
+ return "\(value) is not below the exclusive maximum"
+ }
+ return nil
+ }
+
+ func validateAtomic(_ lexical: String) -> String? {
+ let value = Self.process(lexical, whiteSpace: facets.whiteSpace ?? base.whiteSpace)
+ let primitive = base.primitive
+ guard primitive.isValid(value) else {
+ return "'\(lexical)' is not a valid \(base.rawValue)"
+ }
+ return boundsError(value, primitive)
+ ?? lengthError(value, primitive)
+ ?? digitsError(value)
+ ?? patternError(value)
+ ?? enumerationError(value, primitive)
+ ?? rangeError(value, primitive)
+ }
+}
diff --git a/Sources/Schema/SimpleType.swift b/Sources/Schema/SimpleType.swift
index b3cc7c3..a10312f 100644
--- a/Sources/Schema/SimpleType.swift
+++ b/Sources/Schema/SimpleType.swift
@@ -239,20 +239,6 @@ public extension PureXML.Schema {
return nil
}
- private func validateAtomic(_ lexical: String) -> String? {
- let value = Self.process(lexical, whiteSpace: facets.whiteSpace ?? base.whiteSpace)
- let primitive = base.primitive
- guard primitive.isValid(value) else {
- return "'\(lexical)' is not a valid \(base.rawValue)"
- }
- return boundsError(value, primitive)
- ?? lengthError(value, primitive)
- ?? digitsError(value)
- ?? patternError(value)
- ?? enumerationError(value, primitive)
- ?? rangeError(value, primitive)
- }
-
// MARK: Whitespace
static func process(_ value: String, whiteSpace: WhiteSpace) -> String {
@@ -266,128 +252,6 @@ public extension PureXML.Schema {
return String(replaced).split(separator: " ").joined(separator: " ")
}
}
-
- // MARK: Facet checks
-
- private func boundsError(_ value: String, _: Primitive) -> String? {
- let (lower, upper) = base.bounds
- guard lower != nil || upper != nil, let decimal = DecimalValue(value, allowFraction: false) else {
- return nil
- }
- if let lower, decimal < lower {
- return "\(value) is below the minimum for \(base.rawValue)"
- }
- if let upper, decimal > upper {
- return "\(value) is above the maximum for \(base.rawValue)"
- }
- return nil
- }
-
- private func lengthError(_ value: String, _ primitive: Primitive) -> String? {
- // length/minLength/maxLength measure characters, octets, or list
- // items depending on the type; for QName the spec leaves the unit
- // unspecified (XSD 1.0 Datatypes 4.3.1), so like Xerces and the XSTS
- // NIST oracle we do not constrain QName values by length.
- if case .qName = primitive {
- return nil
- }
- if case .notation = primitive {
- return nil
- }
- let length = primitive.measuredLength(value)
- if let exact = facets.length, length != exact {
- return "length \(length) is not \(exact)"
- }
- if let minimum = facets.minLength, length < minimum {
- return "length \(length) is below \(minimum)"
- }
- if let maximum = facets.maxLength, length > maximum {
- return "length \(length) is above \(maximum)"
- }
- return nil
- }
-
- private func digitsError(_ value: String) -> String? {
- guard facets.totalDigits != nil || facets.fractionDigits != nil,
- let decimal = DecimalValue(value, allowFraction: true) else { return nil }
- if let total = facets.totalDigits, decimal.totalDigits > total {
- return "more than \(total) total digits"
- }
- if let fraction = facets.fractionDigits, decimal.fractionDigits.count > fraction {
- return "more than \(fraction) fraction digits"
- }
- return nil
- }
-
- private func patternError(_ value: String) -> String? {
- for group in facets.patternGroups {
- let matched = group.contains { pattern in
- guard let regex = try? PureXML.Regex.Pattern(pattern) else { return false }
- return regex.matches(value)
- }
- guard matched else {
- let label = group.joined(separator: "|")
- return "'\(value)' does not match pattern '\(label)'"
- }
- }
- return nil
- }
-
- private func enumerationError(_ value: String, _ primitive: Primitive) -> String? {
- guard let enumeration = facets.enumeration else { return nil }
- let whiteSpace = facets.whiteSpace ?? base.whiteSpace
- let matched = enumeration.contains { candidate in
- let normalized = Self.process(candidate, whiteSpace: whiteSpace)
- if let lhs = primitive.ordered(value), let rhs = primitive.ordered(normalized) {
- return lhs == rhs
- }
- return normalized == value
- }
- return matched ? nil : "'\(value)' is not in the enumeration"
- }
-
- private func rangeError(_ value: String, _ primitive: Primitive) -> String? {
- if case .duration = primitive {
- return durationRangeError(value)
- }
- guard let actual = primitive.ordered(value) else { return nil }
- if let bound = facets.minInclusive, let limit = primitive.ordered(bound), actual < limit {
- return "\(value) is below the inclusive minimum"
- }
- if let bound = facets.maxInclusive, let limit = primitive.ordered(bound), actual > limit {
- return "\(value) is above the inclusive maximum"
- }
- if let bound = facets.minExclusive, let limit = primitive.ordered(bound), !(limit < actual) {
- return "\(value) is not above the exclusive minimum"
- }
- if let bound = facets.maxExclusive, let limit = primitive.ordered(bound), !(actual < limit) {
- return "\(value) is not below the exclusive maximum"
- }
- return nil
- }
-
- /// Ordering facets on `xs:duration`, which is a partial order: a value
- /// that is incomparable to the bound does not satisfy it (it is not in
- /// the ordered range), so an incomparable result is a violation.
- private func durationRangeError(_ value: String) -> String? {
- guard let actual = DurationValue(value) else { return nil }
- func order(_ bound: String?) -> DurationOrder? {
- bound.flatMap(DurationValue.init).map(actual.compare(to:))
- }
- if let order = order(facets.minInclusive), order == .lessThan || order == .incomparable {
- return "\(value) is below the inclusive minimum"
- }
- if let order = order(facets.maxInclusive), order == .greaterThan || order == .incomparable {
- return "\(value) is above the inclusive maximum"
- }
- if let order = order(facets.minExclusive), order != .greaterThan {
- return "\(value) is not above the exclusive minimum"
- }
- if let order = order(facets.maxExclusive), order != .lessThan {
- return "\(value) is not below the exclusive maximum"
- }
- return nil
- }
}
/// Validates `value` against a built-in type with no extra facets.
diff --git a/Sources/XSLT/XSLTFunctions.swift b/Sources/XSLT/XSLTFunctions.swift
index 60ccfa8..3ee660b 100644
--- a/Sources/XSLT/XSLTFunctions.swift
+++ b/Sources/XSLT/XSLTFunctions.swift
@@ -389,31 +389,3 @@ extension PureXML.XSLT {
}
}
}
-
-extension PureXML.Emitting.Options {
- /// These options with an `xsl:output`'s settings layered over them. Only an
- /// explicit setting overrides; unspecified `xsl:output` attributes leave the
- /// caller's corresponding option untouched.
- func applying(_ output: PureXML.XSLT.Output) -> Self {
- var copy = self
- if let indent = output.indent {
- copy.prettyPrint = indent
- // Xalan-style indentation: children on their own lines with no
- // leading spaces (indent-amount 0), the conformance golds' form.
- copy.indent = ""
- }
- if let omit = output.omitXMLDeclaration {
- copy.includeXMLDeclaration = !omit
- }
- if let encoding = output.encoding {
- copy.encodingName = encoding
- }
- if let version = output.version {
- copy.xmlVersion = version
- }
- if let standalone = output.standalone {
- copy.standalone = standalone
- }
- return copy
- }
-}
diff --git a/Sources/XSLT/XSLTOutputOptions.swift b/Sources/XSLT/XSLTOutputOptions.swift
new file mode 100644
index 0000000..a3685ad
--- /dev/null
+++ b/Sources/XSLT/XSLTOutputOptions.swift
@@ -0,0 +1,27 @@
+extension PureXML.Emitting.Options {
+ /// These options with an `xsl:output`'s settings layered over them. Only an
+ /// explicit setting overrides; unspecified `xsl:output` attributes leave the
+ /// caller's corresponding option untouched.
+ func applying(_ output: PureXML.XSLT.Output) -> Self {
+ var copy = self
+ if let indent = output.indent {
+ copy.prettyPrint = indent
+ // Xalan-style indentation: children on their own lines with no
+ // leading spaces (indent-amount 0), the conformance golds' form.
+ copy.indent = ""
+ }
+ if let omit = output.omitXMLDeclaration {
+ copy.includeXMLDeclaration = !omit
+ }
+ if let encoding = output.encoding {
+ copy.encodingName = encoding
+ }
+ if let version = output.version {
+ copy.xmlVersion = version
+ }
+ if let standalone = output.standalone {
+ copy.standalone = standalone
+ }
+ return copy
+ }
+}
diff --git a/Sources/XSLT/XSLTParser.swift b/Sources/XSLT/XSLTParser.swift
index 8038c41..afd12dc 100644
--- a/Sources/XSLT/XSLTParser.swift
+++ b/Sources/XSLT/XSLTParser.swift
@@ -223,7 +223,7 @@ extension PureXML.XSLT {
)
}
- private static func withParameters(_ node: XSLTTree) -> [Binding] {
+ static func withParameters(_ node: XSLTTree) -> [Binding] {
XSLTNode.children(node, named: "with-param").map(binding)
}
@@ -247,145 +247,5 @@ extension PureXML.XSLT {
}
return 0
}
-
- // MARK: Bodies and instructions
-
- static func body(_ node: XSLTTree) -> [Instruction] {
- // Adjacent text and CDATA children are one text node in the data
- // model, so a whitespace-only run is dropped (XSLT 1.0 3.4) only when
- // the WHOLE coalesced run is whitespace: " " keeps its
- // spaces because the run "x" surrounds them with non-whitespace.
- // `xml:space="preserve"` in scope keeps a whitespace-only run too.
- let preserve = preservesWhitespace(node)
- var result: [Instruction] = []
- var run = ""
- func flush() {
- if !run.isEmpty, preserve || run.unicodeScalars.contains(where: { !PureXML.Parsing.XMLCharacter.isWhitespace($0) }) {
- result.append(.literalText(run))
- }
- run = ""
- }
- for child in node.children {
- if child.kind == .text || child.kind == .cdata {
- run += child.value
- } else {
- flush()
- if let instruction = instruction(child) {
- result.append(instruction)
- }
- }
- }
- flush()
- return result
- }
-
- static func instruction(_ node: XSLTTree) -> Instruction? {
- switch node.kind {
- case .text, .cdata:
- let value = node.value
- return value.allSatisfy { $0 == " " || $0 == "\t" || $0 == "\n" || $0 == "\r" } ? nil : .literalText(value)
- case .element:
- return XSLTNode.isXSL(node) ? xslInstruction(node) : literalElement(node)
- default:
- return nil
- }
- }
-
- private static func xslInstruction(_ node: XSLTTree) -> Instruction? {
- if let known = simpleInstruction(node) ?? structuralInstruction(node) {
- return known
- }
- // An unrecognized XSLT element instantiates its xsl:fallback children
- // (forwards-compatible processing); with none, it is dropped.
- let fallback = XSLTNode.children(node, named: "fallback").flatMap(body)
- return fallback.isEmpty ? nil : .fallback(body: fallback)
- }
-
- private static func simpleInstruction(_ node: XSLTTree) -> Instruction? {
- switch XSLTNode.localName(node) {
- case "value-of": .valueOf(
- select: XSLTNode.attribute(node, "select") ?? "",
- raw: XSLTNode.attribute(node, "disable-output-escaping") == "yes",
- )
- case "apply-templates": .applyTemplates(
- select: XSLTNode.attribute(node, "select"),
- mode: expandedMode(node),
- sorts: sorts(node),
- parameters: withParameters(node),
- )
- case "copy-of": .copyOf(select: XSLTNode.attribute(node, "select") ?? "")
- case "call-template": .callTemplate(
- name: expandedDeclaredName(node),
- parameters: withParameters(node),
- )
- case "text": .literalText(
- XSLTNode.attribute(node, "disable-output-escaping") == "yes"
- ? PureXML.XSLT.RawText.marked(node.stringValue)
- : node.stringValue,
- )
- case "variable", "param": variable(node)
- case "copy": .copy(useAttributeSets: useAttributeSets(node), body: body(node))
- case "message": .message(terminate: XSLTNode.attribute(node, "terminate") == "yes", body: body(node))
- case "apply-imports": .applyImports
- default: nil
- }
- }
-
- private static func variable(_ node: XSLTTree) -> Instruction {
- .variable(name: expandedDeclaredName(node), select: XSLTNode.attribute(node, "select"), body: body(node))
- }
-
- static func choose(_ node: XSLTTree) -> Instruction {
- let whens = XSLTNode.children(node, named: "when").map { branch in
- Branch(test: XSLTNode.attribute(branch, "test") ?? "", body: body(branch))
- }
- let otherwise = XSLTNode.children(node, named: "otherwise").first.map(body) ?? []
- return .choose(whens: whens, otherwise: otherwise)
- }
-
- private static func literalElement(_ node: XSLTTree) -> Instruction {
- guard let name = node.name else { return .literalText("") }
- // xmlns declarations and the special xsl:* attributes (use-attribute-sets,
- // version, exclude-result-prefixes …) are not copied to the output.
- let attributes = node.attributes
- .filter { $0.name.prefix != "xmlns" && !($0.name.prefix == nil && $0.name.localName == "xmlns") && $0.name.prefix != "xsl" }
- .map { LiteralAttribute(name: $0.name, value: valueTemplate($0.value)) }
- return .literalElement(
- name: name,
- attributes: attributes,
- namespaces: copiedNamespaces(node),
- useAttributeSets: useAttributeSets(node),
- body: body(node),
- )
- }
-
- /// The whitespace-separated names of `[xsl:]use-attribute-sets` on `node`.
- static func useAttributeSets(_ node: XSLTTree) -> [String] {
- // Each referenced set is keyed by expanded QName, matching the
- // expansion applied to the attribute-set declaration's name.
- (XSLTNode.attribute(node, "use-attribute-sets") ?? "")
- .split(whereSeparator: \.isWhitespace)
- .map { expandedQName(String($0), at: node) }
- }
-
- private static func caseOrder(_ node: XSLTTree) -> PureXML.XSLT.CaseOrder? {
- switch XSLTNode.attribute(node, "case-order") {
- case "upper-first": .upperFirst
- case "lower-first": .lowerFirst
- default: nil
- }
- }
-
- static func sorts(_ node: XSLTTree) -> [Sort] {
- XSLTNode.children(node, named: "sort").map { sort in
- Sort(
- select: XSLTNode.attribute(sort, "select") ?? ".",
- descending: XSLTNode.attribute(sort, "order") == "descending",
- numeric: XSLTNode.attribute(sort, "data-type") == "number",
- caseOrder: caseOrder(sort),
- lang: XSLTNode.attribute(sort, "lang").map(valueTemplate),
- )
- }
- }
}
}
diff --git a/Sources/XSLT/XSLTParserInstructions.swift b/Sources/XSLT/XSLTParserInstructions.swift
new file mode 100644
index 0000000..3a7733a
--- /dev/null
+++ b/Sources/XSLT/XSLTParserInstructions.swift
@@ -0,0 +1,143 @@
+/// Instruction and template-body compilation, split out so the parser enum
+/// stays within the type-body and file length budgets.
+extension PureXML.XSLT.XSLTParser {
+ // MARK: Bodies and instructions
+
+ static func body(_ node: XSLTTree) -> [PureXML.XSLT.Instruction] {
+ // Adjacent text and CDATA children are one text node in the data
+ // model, so a whitespace-only run is dropped (XSLT 1.0 3.4) only when
+ // the WHOLE coalesced run is whitespace: " " keeps its
+ // spaces because the run "x" surrounds them with non-whitespace.
+ // `xml:space="preserve"` in scope keeps a whitespace-only run too.
+ let preserve = preservesWhitespace(node)
+ var result: [PureXML.XSLT.Instruction] = []
+ var run = ""
+ func flush() {
+ if !run.isEmpty, preserve || run.unicodeScalars.contains(where: { !PureXML.Parsing.XMLCharacter.isWhitespace($0) }) {
+ result.append(.literalText(run))
+ }
+ run = ""
+ }
+ for child in node.children {
+ if child.kind == .text || child.kind == .cdata {
+ run += child.value
+ } else {
+ flush()
+ if let instruction = instruction(child) {
+ result.append(instruction)
+ }
+ }
+ }
+ flush()
+ return result
+ }
+
+ static func instruction(_ node: XSLTTree) -> PureXML.XSLT.Instruction? {
+ switch node.kind {
+ case .text, .cdata:
+ let value = node.value
+ return value.allSatisfy { $0 == " " || $0 == "\t" || $0 == "\n" || $0 == "\r" } ? nil : .literalText(value)
+ case .element:
+ return XSLTNode.isXSL(node) ? xslInstruction(node) : literalElement(node)
+ default:
+ return nil
+ }
+ }
+
+ private static func xslInstruction(_ node: XSLTTree) -> PureXML.XSLT.Instruction? {
+ if let known = simpleInstruction(node) ?? structuralInstruction(node) {
+ return known
+ }
+ // An unrecognized XSLT element instantiates its xsl:fallback children
+ // (forwards-compatible processing); with none, it is dropped.
+ let fallback = XSLTNode.children(node, named: "fallback").flatMap(body)
+ return fallback.isEmpty ? nil : .fallback(body: fallback)
+ }
+
+ private static func simpleInstruction(_ node: XSLTTree) -> PureXML.XSLT.Instruction? {
+ switch XSLTNode.localName(node) {
+ case "value-of": .valueOf(
+ select: XSLTNode.attribute(node, "select") ?? "",
+ raw: XSLTNode.attribute(node, "disable-output-escaping") == "yes",
+ )
+ case "apply-templates": .applyTemplates(
+ select: XSLTNode.attribute(node, "select"),
+ mode: expandedMode(node),
+ sorts: sorts(node),
+ parameters: withParameters(node),
+ )
+ case "copy-of": .copyOf(select: XSLTNode.attribute(node, "select") ?? "")
+ case "call-template": .callTemplate(
+ name: expandedDeclaredName(node),
+ parameters: withParameters(node),
+ )
+ case "text": .literalText(
+ XSLTNode.attribute(node, "disable-output-escaping") == "yes"
+ ? PureXML.XSLT.RawText.marked(node.stringValue)
+ : node.stringValue,
+ )
+ case "variable", "param": variable(node)
+ case "copy": .copy(useAttributeSets: useAttributeSets(node), body: body(node))
+ case "message": .message(terminate: XSLTNode.attribute(node, "terminate") == "yes", body: body(node))
+ case "apply-imports": .applyImports
+ default: nil
+ }
+ }
+
+ static func variable(_ node: XSLTTree) -> PureXML.XSLT.Instruction {
+ .variable(name: expandedDeclaredName(node), select: XSLTNode.attribute(node, "select"), body: body(node))
+ }
+
+ static func choose(_ node: XSLTTree) -> PureXML.XSLT.Instruction {
+ let whens = XSLTNode.children(node, named: "when").map { branch in
+ PureXML.XSLT.Branch(test: XSLTNode.attribute(branch, "test") ?? "", body: body(branch))
+ }
+ let otherwise = XSLTNode.children(node, named: "otherwise").first.map(body) ?? []
+ return .choose(whens: whens, otherwise: otherwise)
+ }
+
+ private static func literalElement(_ node: XSLTTree) -> PureXML.XSLT.Instruction {
+ guard let name = node.name else { return .literalText("") }
+ // xmlns declarations and the special xsl:* attributes (use-attribute-sets,
+ // version, exclude-result-prefixes …) are not copied to the output.
+ let attributes = node.attributes
+ .filter { $0.name.prefix != "xmlns" && !($0.name.prefix == nil && $0.name.localName == "xmlns") && $0.name.prefix != "xsl" }
+ .map { PureXML.XSLT.LiteralAttribute(name: $0.name, value: valueTemplate($0.value)) }
+ return .literalElement(
+ name: name,
+ attributes: attributes,
+ namespaces: copiedNamespaces(node),
+ useAttributeSets: useAttributeSets(node),
+ body: body(node),
+ )
+ }
+
+ /// The whitespace-separated names of `[xsl:]use-attribute-sets` on `node`.
+ static func useAttributeSets(_ node: XSLTTree) -> [String] {
+ // Each referenced set is keyed by expanded QName, matching the
+ // expansion applied to the attribute-set declaration's name.
+ (XSLTNode.attribute(node, "use-attribute-sets") ?? "")
+ .split(whereSeparator: \.isWhitespace)
+ .map { expandedQName(String($0), at: node) }
+ }
+
+ private static func caseOrder(_ node: XSLTTree) -> PureXML.XSLT.CaseOrder? {
+ switch XSLTNode.attribute(node, "case-order") {
+ case "upper-first": .upperFirst
+ case "lower-first": .lowerFirst
+ default: nil
+ }
+ }
+
+ static func sorts(_ node: XSLTTree) -> [PureXML.XSLT.Sort] {
+ XSLTNode.children(node, named: "sort").map { sort in
+ PureXML.XSLT.Sort(
+ select: XSLTNode.attribute(sort, "select") ?? ".",
+ descending: XSLTNode.attribute(sort, "order") == "descending",
+ numeric: XSLTNode.attribute(sort, "data-type") == "number",
+ caseOrder: caseOrder(sort),
+ lang: XSLTNode.attribute(sort, "lang").map(valueTemplate),
+ )
+ }
+ }
+}