PureXML/Sources/Emitting/Escaping.swift
Mihaela Mihaljevic e2ba6a7359 feat(emitting): cdataAsText and asciiOnly save options
- cdataAsText emits a CDATA section's content as escaped text instead of
  a <![CDATA[ ... ]]> section.
- asciiOnly escapes every non-ASCII character as a numeric character
  reference, so the output is pure ASCII (libxml2's character-reference
  encoding).

Both apply to the Serializer and the incremental Writer through the
shared Escaping rules.

Closes #91
2026-06-08 16:34:57 +02:00

53 lines
2.2 KiB
Swift

extension PureXML.Emitting {
/// The shared XML output escaping rules, used by both the tree serializer and
/// the incremental writer so their output is identical. Element content
/// escapes the markup characters; attribute values additionally escape the
/// quote and the whitespace characters as numeric references (matching
/// libxml2), so attributes survive attribute-value normalization.
enum Escaping {
static func text(_ value: String, asciiOnly: Bool = false) -> String {
var result = ""
for character in value {
switch character {
case "&": result += "&amp;"
case "<": result += "&lt;"
case ">": result += "&gt;"
default: result += plain(character, asciiOnly: asciiOnly)
}
}
return result
}
/// A character verbatim, or, in ASCII-only mode, each of its non-ASCII
/// scalars as a numeric character reference so the output is pure ASCII.
private static func plain(_ character: Character, asciiOnly: Bool) -> String {
guard asciiOnly else { return String(character) }
var result = ""
for scalar in character.unicodeScalars {
result += scalar.value > 0x7F ? "&#x\(String(scalar.value, radix: 16, uppercase: true));" : String(scalar)
}
return result
}
static func attribute(_ value: String, quote: Character = "\"", asciiOnly: Bool = false) -> String {
var result = ""
for character in value {
if character == quote {
result += quote == "'" ? "&apos;" : "&quot;"
continue
}
switch character {
case "&": result += "&amp;"
case "<": result += "&lt;"
case ">": result += "&gt;"
case "\t": result += "&#9;"
case "\n": result += "&#10;"
case "\r": result += "&#13;"
default: result += plain(character, asciiOnly: asciiOnly)
}
}
return result
}
}
}