refactor: split oversized types across companion files for lint budgets
This commit is contained in:
parent
b0c64d86b2
commit
ca65a334f7
@ -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 {
|
||||
|
||||
75
Sources/HTML/HTMLDocumentTableAndForeign.swift
Normal file
75
Sources/HTML/HTMLDocumentTableAndForeign.swift
Normal 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
|
||||
}
|
||||
}
|
||||
5
Sources/HTML/HTMLForeignNamespace.swift
Normal file
5
Sources/HTML/HTMLForeignNamespace.swift
Normal 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"
|
||||
}
|
||||
@ -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
|
||||
|
||||
98
Sources/Parsing/EventReaderText.swift
Normal file
98
Sources/Parsing/EventReaderText.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
147
Sources/Parsing/StreamingDecoderCJK.swift
Normal file
147
Sources/Parsing/StreamingDecoderCJK.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
91
Sources/Schema/RelaxNGStreaming.swift
Normal file
91
Sources/Schema/RelaxNGStreaming.swift
Normal 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.
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
176
Sources/Schema/SchemaSimpleContentCompatibility.swift
Normal file
176
Sources/Schema/SchemaSimpleContentCompatibility.swift
Normal 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
|
||||
}
|
||||
}
|
||||
137
Sources/Schema/SchemaSimpleTypeFacetChecks.swift
Normal file
137
Sources/Schema/SchemaSimpleTypeFacetChecks.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
27
Sources/XSLT/XSLTOutputOptions.swift
Normal file
27
Sources/XSLT/XSLTOutputOptions.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
143
Sources/XSLT/XSLTParserInstructions.swift
Normal file
143
Sources/XSLT/XSLTParserInstructions.swift
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user