PureXML/Sources/Parsing/XMLCharacter.swift
Mihaela Mihaljevic a041b8beca feat(dtd): attribute defaulting, tokenized types, value normalization
Complete the DTD attribute handling found in the audit:

- Default and #FIXED attribute values are applied to the parsed tree.
  parseApplyingInternalDTDDefaults returns a tree where every element
  carries the attributes its <!ATTLIST> defaults, so an editor sees the
  same effective attributes a validating processor would.
- NMTOKEN, NMTOKENS, ENTITY, ENTITIES are validated: NMTOKEN(S) must be
  XML name tokens, ENTITY/ENTITIES must be XML names. Previously all fell
  through to unconstrained CDATA.
- DTD default values are normalized per XML 1.0: character and predefined
  entity references are expanded, literal whitespace becomes a space, and
  non-CDATA values are collapsed and trimmed.

ENTITY/ENTITIES check name syntax only; that the named entity is a
declared unparsed entity is not verified, since NDATA entity tracking is
not yet modeled.

Closes #73
2026-06-08 14:31:42 +02:00

87 lines
3.0 KiB
Swift

public extension PureXML.Parsing {
/// Character classification per the XML 1.0 (Fifth Edition) grammar, the
/// single source of truth for what bytes are legal where. Predicates operate
/// on Unicode scalars, since the grammar productions are defined over code
/// points, not graphemes.
enum XMLCharacter {
/// `S`: white space (space, tab, carriage return, line feed).
public static func isWhitespace(_ scalar: Unicode.Scalar) -> Bool {
switch scalar.value {
case 0x20, 0x09, 0x0D, 0x0A: true
default: false
}
}
/// `Char`: any legal XML character.
public static func isChar(_ scalar: Unicode.Scalar) -> Bool {
switch scalar.value {
case 0x09, 0x0A, 0x0D,
0x20 ... 0xD7FF,
0xE000 ... 0xFFFD,
0x10000 ... 0x10FFFF:
true
default:
false
}
}
/// `NameStartChar`: the first character of a name.
public static func isNameStart(_ scalar: Unicode.Scalar) -> Bool {
switch scalar.value {
case 0x3A, // ":"
0x41 ... 0x5A, // A-Z
0x5F, // "_"
0x61 ... 0x7A, // a-z
0xC0 ... 0xD6,
0xD8 ... 0xF6,
0xF8 ... 0x2FF,
0x370 ... 0x37D,
0x37F ... 0x1FFF,
0x200C ... 0x200D,
0x2070 ... 0x218F,
0x2C00 ... 0x2FEF,
0x3001 ... 0xD7FF,
0xF900 ... 0xFDCF,
0xFDF0 ... 0xFFFD,
0x10000 ... 0xEFFFF:
true
default:
false
}
}
/// `NameChar`: a name character after the first.
public static func isNameChar(_ scalar: Unicode.Scalar) -> Bool {
if isNameStart(scalar) { return true }
switch scalar.value {
case 0x2D, // "-"
0x2E, // "."
0x30 ... 0x39, // 0-9
0xB7,
0x300 ... 0x36F,
0x203F ... 0x2040:
return true
default:
return false
}
}
/// `Name`: a NameStartChar followed by zero or more NameChar.
public static func isValidName(_ name: String) -> Bool {
var scalars = name.unicodeScalars.makeIterator()
guard let first = scalars.next(), isNameStart(first) else { return false }
while let scalar = scalars.next() {
if !isNameChar(scalar) { return false }
}
return true
}
/// `Nmtoken`: one or more NameChar (no name-start requirement).
public static func isNmtoken(_ token: String) -> Bool {
let scalars = token.unicodeScalars
guard !scalars.isEmpty else { return false }
return scalars.allSatisfy(isNameChar)
}
}
}