The serializer's escaping functions rebuilt every text, attribute, comment, and processing-instruction value character by character, even when the value contained nothing to escape, which is the common case. Each now scans the value's UTF-8 bytes for the bytes it would escape (the markup characters, the active attribute quote, the whitespace characters, a comment hyphen, a PI question mark, and any non-ASCII byte in ASCII-only mode) and returns the value unchanged when none is present, taking the original character path only when something must be escaped. Measured serialize roughly halved, ~5.5x to ~2.7x slower than libxml2 on the 20k-item generated corpus, with byte-identical output. EscapingFastPathTests pins both the unchanged fast path and the escaped slow path for each function, and the Apache Xalan XSLT gold-output corpus (a serialize-heavy differential) stays green. No output changed.
52 lines
2.6 KiB
Swift
52 lines
2.6 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// The escaping functions take a fast path that returns the value unchanged when
|
|
/// nothing needs escaping, and the original character-by-character path when
|
|
/// something does. These tests pin both: a value with no escapable character
|
|
/// must round-trip identically (and, being the fast path, be the same instance's
|
|
/// content), and a value with an escapable character must still be escaped.
|
|
@Suite("Escaping fast-path equivalence")
|
|
struct EscapingFastPathTests {
|
|
private typealias Escaping = PureXML.Emitting.Escaping
|
|
|
|
@Test("text: plain content is unchanged, markup is escaped")
|
|
func test_text() {
|
|
#expect(Escaping.text("plain content, no markup") == "plain content, no markup")
|
|
#expect(Escaping.text("a & b < c > d") == "a & b < c > d")
|
|
#expect(Escaping.text("café résumé") == "café résumé")
|
|
// ASCII-only escapes non-ASCII even with no markup character.
|
|
#expect(Escaping.text("café", asciiOnly: true) == "café")
|
|
// A carriage return is escaped only when asked.
|
|
#expect(Escaping.text("a\rb") == "a\rb")
|
|
#expect(Escaping.text("a\rb", escapeCarriageReturn: true) == "a
b")
|
|
}
|
|
|
|
@Test("attribute: plain value is unchanged, specials are escaped")
|
|
func test_attribute() {
|
|
#expect(Escaping.attribute("plain value") == "plain value")
|
|
#expect(Escaping.attribute("a \"q\" b") == "a "q" b")
|
|
// The inactive quote is not escaped.
|
|
#expect(Escaping.attribute("it's fine", quote: "\"") == "it's fine")
|
|
#expect(Escaping.attribute("it's fine", quote: "'") == "it's fine")
|
|
#expect(Escaping.attribute("x\ty\nz") == "x	y z")
|
|
#expect(Escaping.attribute("a & b < c") == "a & b < c")
|
|
#expect(Escaping.attribute("ñ", asciiOnly: true) == "ñ")
|
|
}
|
|
|
|
@Test("comment: hyphen-free is unchanged, -- gets a separating space")
|
|
func test_comment() {
|
|
#expect(Escaping.comment("a plain comment") == "a plain comment")
|
|
#expect(Escaping.comment("single - hyphen is fine") == "single - hyphen is fine")
|
|
#expect(Escaping.comment("bad -- pair") == "bad - - pair")
|
|
#expect(Escaping.comment("trailing-") == "trailing- ")
|
|
}
|
|
|
|
@Test("processing instruction: question-free is unchanged, ?> gets a space")
|
|
func test_processingInstruction() {
|
|
#expect(Escaping.processingInstruction("plain data") == "plain data")
|
|
#expect(Escaping.processingInstruction("a ? b") == "a ? b")
|
|
#expect(Escaping.processingInstruction("a?>b") == "a? >b")
|
|
}
|
|
}
|