PureXML/Sources/XSLT/XSLTRawText.swift
Mihaela Mihaljevic 5a0c2ec7e4 feat(xslt): output methods in depth
disable-output-escaping on xsl:value-of and xsl:text (sentinel-marked
raw text, unescaped after serialization; string extraction from a
fragment drops the markers per 16.4), the html default method for a
null-namespace html root (16.1), the spec-default XML declaration for
the xml method, META Content-Type injection into head (16.2),
case-insensitive raw-text and void element classes, and HTML 4.01
Latin-1 named entities on html output. Whitespace-only stripping now
means XML whitespace, so NBSP-only text survives. Conf comparison
treats tag-adjacent whitespace as layout outside the canonical path.
Xalan baseline 371 -> 352 (output 49 -> 30).
2026-06-10 13:10:50 +02:00

73 lines
2.8 KiB
Swift

extension PureXML.XSLT {
/// `disable-output-escaping`: raw text travels through the result tree
/// bracketed by private-use sentinels, and after serialization the
/// escaping applied inside the brackets is undone (XSLT 1.0 section 16.4,
/// the serializer-flag technique).
enum RawText {
static let begin: Character = "\u{E000}"
static let end: Character = "\u{E001}"
static func marked(_ text: String) -> String {
String(begin) + text + String(end)
}
/// The text with any raw markers removed (escaping re-enabled).
static func stripped(_ text: String) -> String {
guard text.contains(begin) || text.contains(end) else { return text }
return String(text.filter { $0 != begin && $0 != end })
}
/// Removes the sentinels, undoing the serializer's escaping between
/// them. Text outside sentinel pairs passes through untouched.
static func resolve(_ serialized: String) -> String {
guard serialized.contains(begin) else { return serialized }
var result = ""
var rawRegion = ""
var inRaw = false
for character in serialized {
switch character {
case begin:
inRaw = true
case end:
result += unescaped(rawRegion)
rawRegion = ""
inRaw = false
default:
if inRaw {
rawRegion.append(character)
} else {
result.append(character)
}
}
}
return result + unescaped(rawRegion)
}
/// The serializer's own escape forms, decoded back.
private static let escapes: [String: Character] = [
"quot": "\"", "lt": "<", "gt": ">", "amp": "&",
"#xD": "\r", "#x9": "\t", "#xA": "\n", "#13": "\r", "#9": "\t", "#10": "\n",
]
/// Undoes the XML/HTML serializer escapes.
private static func unescaped(_ text: String) -> String {
guard text.contains("&") else { return text }
var result = ""
var index = text.startIndex
while index < text.endIndex {
guard text[index] == "&",
let semicolon = text[index...].firstIndex(of: ";"),
let decoded = escapes[String(text[text.index(after: index) ..< semicolon])]
else {
result.append(text[index])
index = text.index(after: index)
continue
}
result.append(decoded)
index = text.index(after: semicolon)
}
return result
}
}
}