The two monolithic XSD content/identity rules only satisfied the OpenAPIKit validation idiom at the wrong granularity: no per-constraint removability, no isolable failing/near-miss tests. Split them into 32 named rules, each finding tagged at its origin and cached per XSDContext so the walk still runs once per document (the #101 decision against duplicating XSD's recursive type resolution still holds). Wired the streaming path's ID/IDREF and identity checks through the same framework instead of calling the validators imperatively, and added the unwrapAndLookup combinator the rule already named as required. Also includes the mandated repo-wide swiftformat/swiftlint pass.
79 lines
3.3 KiB
Swift
79 lines
3.3 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
@Suite("HTML tokenizer: references, NUL, RCDATA")
|
|
struct HTMLTokenizerTests {
|
|
/// The concatenated text content of the first parsed element.
|
|
private func text(_ html: String) -> String {
|
|
guard case let .document(children) = PureXML.HTML.parse(html), case let .element(element) = children.first else { return "" }
|
|
return element.children.compactMap {
|
|
if case let .text(value) = $0 {
|
|
value
|
|
} else {
|
|
nil
|
|
}
|
|
}.joined()
|
|
}
|
|
|
|
@Test("A numeric reference to zero, a surrogate, or an out-of-range value yields U+FFFD")
|
|
func test_numericInvalid() {
|
|
#expect(text("<p>�</p>") == "\u{FFFD}")
|
|
#expect(text("<p>�</p>") == "\u{FFFD}")
|
|
#expect(text("<p>�</p>") == "\u{FFFD}")
|
|
}
|
|
|
|
@Test("A numeric reference in the C1 range is mapped to its Windows-1252 character")
|
|
func test_numericC1() {
|
|
#expect(text("<p>€</p>") == "\u{20AC}") // euro
|
|
#expect(text("<p>™</p>") == "\u{2122}") // trade mark
|
|
}
|
|
|
|
@Test("A valid numeric reference resolves to its code point")
|
|
func test_numericValid() {
|
|
#expect(text("<p>AB</p>") == "AB")
|
|
}
|
|
|
|
@Test("A literal NUL byte becomes U+FFFD")
|
|
func test_nullReplacement() {
|
|
#expect(text("<p>a\u{0}b</p>") == "a\u{FFFD}b")
|
|
}
|
|
|
|
@Test("A named reference without a trailing semicolon is still decoded")
|
|
func test_semicolonless() {
|
|
#expect(text("<p>&</p>") == "&")
|
|
#expect(text("<p>©</p>") == "\u{A9}")
|
|
}
|
|
|
|
@Test("RCDATA content (title, textarea) decodes references; raw text (style) does not")
|
|
func test_rcdataDecoding() {
|
|
#expect(text("<title>a & b</title>") == "a & b")
|
|
#expect(text("<textarea><x></textarea>") == "<x>")
|
|
#expect(text("<style>a & b</style>") == "a & b")
|
|
}
|
|
|
|
@Test("A single leading newline after <textarea> is stripped")
|
|
func test_textareaLeadingNewline() {
|
|
#expect(text("<textarea>\nhello</textarea>") == "hello")
|
|
#expect(text("<textarea>\n\nhello</textarea>") == "\nhello")
|
|
}
|
|
|
|
@Test("The HTML4 named entity set decodes across its categories")
|
|
func test_namedEntitySet() {
|
|
#expect(text("<p>½Á×</p>") == "\u{BD}\u{C1}\u{D7}") // Latin-1
|
|
#expect(text("<p>Œ†—</p>") == "\u{152}\u{2020}\u{2014}") // special
|
|
#expect(text("<p>αΩπ</p>") == "\u{3B1}\u{3A9}\u{3C0}") // Greek
|
|
#expect(text("<p>∀∑∞≠</p>") == "\u{2200}\u{2211}\u{221E}\u{2260}") // math
|
|
#expect(text("<p>←→⇔</p>") == "\u{2190}\u{2192}\u{21D4}") // arrows
|
|
#expect(text("<p>♥♦♠</p>") == "\u{2665}\u{2666}\u{2660}") // suits
|
|
#expect(text("<p>© ®</p>") == "\u{A9} \u{AE}") // semicolon-less
|
|
}
|
|
|
|
@Test("The full WHATWG reference set decodes astral and multi-codepoint names")
|
|
func test_fullEntitySet() {
|
|
#expect(PureXML.HTML.Tokenizer.namedEntities.count == 2125)
|
|
#expect(text("<p>𝕗</p>") == "\u{1D557}") // astral double-struck f
|
|
#expect(text("<p>≂̸</p>") == "\u{2242}\u{0338}") // two codepoints
|
|
#expect(text("<p>∳</p>") == "\u{2233}") // 31-char name
|
|
}
|
|
}
|