Add the PureXML.HTML namespace: a lenient tokenizer for tag-soup input (case-insensitive tags; quoted, unquoted, and boolean attributes; comments; doctype; character references), a tree builder handling void elements, optional end tags (li, p, table rows and cells), raw-text elements (script, style), and unmatched end tags, and a serializer that writes void elements unclosed and raw-text content unescaped. Closes #36 Closes #37 Closes #38 Closes #39 Closes #20
70 lines
2.7 KiB
Swift
70 lines
2.7 KiB
Swift
@testable import PureXML
|
|
import Testing
|
|
|
|
@Suite("HTML")
|
|
struct HTMLTests {
|
|
private func roundTrip(_ html: String) -> String {
|
|
PureXML.HTML.serialize(PureXML.HTML.parse(html))
|
|
}
|
|
|
|
@Test("Void elements need no end tag and serialize without a slash")
|
|
func test_voidElements() {
|
|
#expect(roundTrip("<p>a<br>b<img src=\"x\">c</p>") == "<p>a<br>b<img src=\"x\">c</p>")
|
|
}
|
|
|
|
@Test("Optional end tags close implicitly")
|
|
func test_optionalEndTags() {
|
|
#expect(roundTrip("<ul><li>one<li>two</ul>") == "<ul><li>one</li><li>two</li></ul>")
|
|
#expect(roundTrip("<p>first<p>second") == "<p>first</p><p>second</p>")
|
|
}
|
|
|
|
@Test("Table rows and cells close implicitly")
|
|
func test_tableImpliedClose() {
|
|
let html = "<table><tr><td>a<td>b<tr><td>c</table>"
|
|
#expect(roundTrip(html) == "<table><tr><td>a</td><td>b</td></tr><tr><td>c</td></tr></table>")
|
|
}
|
|
|
|
@Test("Raw-text elements keep their content unescaped")
|
|
func test_rawText() {
|
|
let html = "<script>if (a < b && c > d) {}</script>"
|
|
let parsed = PureXML.HTML.parse(html)
|
|
#expect(PureXML.HTML.serialize(parsed) == html)
|
|
}
|
|
|
|
@Test("Attributes parse quoted, unquoted, and boolean forms")
|
|
func test_attributes() {
|
|
let parsed = PureXML.HTML.parse("<input type=text value='hi' disabled>")
|
|
guard case let .document(children) = parsed, case let .element(element) = children.first else {
|
|
Issue.record("expected an element")
|
|
return
|
|
}
|
|
#expect(element.attributes.count == 3)
|
|
#expect(element.attributes.contains { $0.name.description == "type" && $0.value == "text" })
|
|
#expect(element.attributes.contains { $0.name.description == "disabled" && $0.value == "" })
|
|
}
|
|
|
|
@Test("Tag names are lowercased")
|
|
func test_caseInsensitive() {
|
|
#expect(roundTrip("<DIV><SPAN>x</SPAN></DIV>") == "<div><span>x</span></div>")
|
|
}
|
|
|
|
@Test("Character references are decoded and re-escaped")
|
|
func test_entities() {
|
|
#expect(roundTrip("<p>a & b < c A</p>") == "<p>a & b < c A</p>")
|
|
#expect(roundTrip("<p> ©</p>") == "<p>\u{A0}\u{A9}</p>")
|
|
}
|
|
|
|
@Test("Comments and an unmatched end tag are handled leniently")
|
|
func test_lenient() {
|
|
#expect(roundTrip("<div><!-- note -->text</div>") == "<div><!-- note -->text</div>")
|
|
#expect(roundTrip("<b>bold</i></b>") == "<b>bold</b>")
|
|
}
|
|
|
|
@Test("A representative document round-trips")
|
|
func test_document() {
|
|
let html = "<html><head><title>T</title></head>"
|
|
+ "<body><h1>Hi</h1><p>Para with <a href=\"/x\">link</a>.</p></body></html>"
|
|
#expect(roundTrip(html) == html)
|
|
}
|
|
}
|