refactor: split oversized types across companion files for lint budgets

This commit is contained in:
Mihaela Mihaljevic 2026-08-31 05:00:44 +02:00
parent b0c64d86b2
commit ca65a334f7
17 changed files with 903 additions and 888 deletions

View File

@ -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 `<select>` (the "in select" rule): a nested select,
/// or an input/keygen/textarea.
private static let closesSelect: Set<String> = ["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<String> = ["caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"]
/// The open-stack index of a `<select>` 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[..<selectIndex].contains(where: { tagName($0) == "table" }) else { return nil }
return selectIndex
}
private func bodyOpen(_ name: String, _ attributes: [(String, String)], selfClosing: Bool) {
if Self.closesSelect.contains(name), let selectIndex = openBody.lastIndex(where: { tagName($0) == "select" }), selectIndex >= 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<String> = ["foreignobject", "desc", "title"]
}
extension HTMLDocument {

View File

@ -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 `<select>` (the "in select" rule): a nested select,
/// or an input/keygen/textarea.
static let closesSelect: Set<String> = ["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<String> = ["caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"]
/// The SVG HTML integration points: their content is parsed as HTML.
private static let svgIntegrationPoints: Set<String> = ["foreignobject", "desc", "title"]
/// The open-stack index of a `<select>` 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[..<selectIndex].contains(where: { tagName($0) == "table" }) else { return nil }
return selectIndex
}
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.
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.
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
}
}

View File

@ -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"
}

View File

@ -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 `<?target+++?>` 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

View File

@ -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 `<?target+++?>` 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)
}
}

View File

@ -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

View File

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

View File

@ -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 <e/>.
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.
}
}

View File

@ -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 <e/>.
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.
}
}

View File

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

View File

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

View File

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

View File

@ -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.

View File

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

View File

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

View File

@ -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: " <![CDATA[x]]> " 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),
)
}
}
}
}

View File

@ -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: " <![CDATA[x]]> " 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),
)
}
}
}