feat(html): full HTML 4.01 named entity set (#84)
This commit is contained in:
parent
60fc0805bc
commit
7eb33c0d08
@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- HTML named character references: the full HTML 4.01 entity set (#84), about 250 references across the Latin-1, special, and symbol (Greek, mathematical, arrow, card-suit) groups, replacing the previous 16-entry stopgap. Decoding stays longest-match with an optional trailing semicolon and the existing numeric-reference validation. This matches libxml2's entity coverage; the much larger WHATWG superset of mostly-astral mathematical references is not vendored.
|
||||
- HTML5 select content model (#83, stage 10): a `<select>` now follows the in-select rules. `<optgroup>` closes an open `<option>`, a nested `<select>` closes the open one rather than nesting, and an `<input>`/`<keygen>`/`<textarea>` closes the select and is placed after it. In the "in select in table" case, a table-structural tag (`caption`/`table`/`tbody`/`tfoot`/`thead`/`tr`/`td`/`th`) closes a select that is inside a table before being processed.
|
||||
- HTML5 frameset documents (#83, stage 8): a `<frameset>` after the head now produces a frameset document whose `frameset` element replaces the body (`<html><head></head><frameset>...`), with nested framesets and `<noframes>` handled, and `<frame>` is now a void element (it no longer wrongly nests). An ordinary document still produces a body.
|
||||
- HTML5 integration points (#83, stage 7): HTML content inside an SVG integration point (`foreignObject`, `desc`, `title`) is now parsed in the HTML namespace rather than SVG, so `<svg><foreignObject><div>` puts the `div` (and its descendants) back in HTML; re-entering `<svg>` inside an integration point switches back to the SVG namespace.
|
||||
|
||||
@ -89,7 +89,7 @@ flowchart TB
|
||||
|
||||
PureXML is a working, dependency-free XML library today: parse, emit, validate,
|
||||
query, and stream documents on macOS, Linux, Windows, and WASM. The test suite
|
||||
currently runs **757 tests in 117 suites** (`swift test`).
|
||||
currently runs **758 tests in 117 suites** (`swift test`).
|
||||
|
||||
### Shipped (libxml2-aligned surface)
|
||||
|
||||
|
||||
@ -1,11 +1,4 @@
|
||||
extension PureXML.HTML.Tokenizer {
|
||||
private static let named: [String: Character] = [
|
||||
"amp": "&", "lt": "<", "gt": ">", "quot": "\"", "apos": "'",
|
||||
"nbsp": "\u{A0}", "copy": "\u{A9}", "reg": "\u{AE}", "trade": "\u{2122}",
|
||||
"mdash": "\u{2014}", "ndash": "\u{2013}", "hellip": "\u{2026}",
|
||||
"laquo": "\u{AB}", "raquo": "\u{BB}", "deg": "\u{B0}", "euro": "\u{20AC}",
|
||||
]
|
||||
|
||||
/// The HTML numeric-character-reference fixups for the Windows-1252 C1 range
|
||||
/// (`0x80`–`0x9F`): a reference to one of these code points yields the mapped
|
||||
/// character rather than the C1 control, per the HTML standard.
|
||||
@ -99,7 +92,7 @@ extension PureXML.HTML.Tokenizer {
|
||||
}
|
||||
// Longest match wins: shrink the candidate until it names an entity.
|
||||
while !name.isEmpty {
|
||||
if let character = named[name] {
|
||||
if let character = namedEntities[name] {
|
||||
let end = start + 1 + name.count
|
||||
let semicolon = end < characters.count && characters[end] == ";"
|
||||
return (String(character), name.count + 1 + (semicolon ? 1 : 0))
|
||||
|
||||
75
Sources/HTML/HTMLEntityTable.swift
Normal file
75
Sources/HTML/HTMLEntityTable.swift
Normal file
@ -0,0 +1,75 @@
|
||||
extension PureXML.HTML.Tokenizer {
|
||||
/// The HTML 4.01 named character references (the `lat1`, `special`, and
|
||||
/// `symbol` entity sets), keyed by the bare entity name. This is the stable,
|
||||
/// fully-specified set (the same coverage libxml2 ships); the much larger
|
||||
/// WHATWG superset of mostly-astral mathematical references is not included.
|
||||
/// The decoder treats the trailing semicolon as optional (lenient tag soup).
|
||||
static let namedEntities: [String: Character] = [
|
||||
// ISO-8859-1 (Latin-1) supplement.
|
||||
"nbsp": "\u{00A0}", "iexcl": "\u{00A1}", "cent": "\u{00A2}", "pound": "\u{00A3}",
|
||||
"curren": "\u{00A4}", "yen": "\u{00A5}", "brvbar": "\u{00A6}", "sect": "\u{00A7}",
|
||||
"uml": "\u{00A8}", "copy": "\u{00A9}", "ordf": "\u{00AA}", "laquo": "\u{00AB}",
|
||||
"not": "\u{00AC}", "shy": "\u{00AD}", "reg": "\u{00AE}", "macr": "\u{00AF}",
|
||||
"deg": "\u{00B0}", "plusmn": "\u{00B1}", "sup2": "\u{00B2}", "sup3": "\u{00B3}",
|
||||
"acute": "\u{00B4}", "micro": "\u{00B5}", "para": "\u{00B6}", "middot": "\u{00B7}",
|
||||
"cedil": "\u{00B8}", "sup1": "\u{00B9}", "ordm": "\u{00BA}", "raquo": "\u{00BB}",
|
||||
"frac14": "\u{00BC}", "frac12": "\u{00BD}", "frac34": "\u{00BE}", "iquest": "\u{00BF}",
|
||||
"Agrave": "\u{00C0}", "Aacute": "\u{00C1}", "Acirc": "\u{00C2}", "Atilde": "\u{00C3}",
|
||||
"Auml": "\u{00C4}", "Aring": "\u{00C5}", "AElig": "\u{00C6}", "Ccedil": "\u{00C7}",
|
||||
"Egrave": "\u{00C8}", "Eacute": "\u{00C9}", "Ecirc": "\u{00CA}", "Euml": "\u{00CB}",
|
||||
"Igrave": "\u{00CC}", "Iacute": "\u{00CD}", "Icirc": "\u{00CE}", "Iuml": "\u{00CF}",
|
||||
"ETH": "\u{00D0}", "Ntilde": "\u{00D1}", "Ograve": "\u{00D2}", "Oacute": "\u{00D3}",
|
||||
"Ocirc": "\u{00D4}", "Otilde": "\u{00D5}", "Ouml": "\u{00D6}", "times": "\u{00D7}",
|
||||
"Oslash": "\u{00D8}", "Ugrave": "\u{00D9}", "Uacute": "\u{00DA}", "Ucirc": "\u{00DB}",
|
||||
"Uuml": "\u{00DC}", "Yacute": "\u{00DD}", "THORN": "\u{00DE}", "szlig": "\u{00DF}",
|
||||
"agrave": "\u{00E0}", "aacute": "\u{00E1}", "acirc": "\u{00E2}", "atilde": "\u{00E3}",
|
||||
"auml": "\u{00E4}", "aring": "\u{00E5}", "aelig": "\u{00E6}", "ccedil": "\u{00E7}",
|
||||
"egrave": "\u{00E8}", "eacute": "\u{00E9}", "ecirc": "\u{00EA}", "euml": "\u{00EB}",
|
||||
"igrave": "\u{00EC}", "iacute": "\u{00ED}", "icirc": "\u{00EE}", "iuml": "\u{00EF}",
|
||||
"eth": "\u{00F0}", "ntilde": "\u{00F1}", "ograve": "\u{00F2}", "oacute": "\u{00F3}",
|
||||
"ocirc": "\u{00F4}", "otilde": "\u{00F5}", "ouml": "\u{00F6}", "divide": "\u{00F7}",
|
||||
"oslash": "\u{00F8}", "ugrave": "\u{00F9}", "uacute": "\u{00FA}", "ucirc": "\u{00FB}",
|
||||
"uuml": "\u{00FC}", "yacute": "\u{00FD}", "thorn": "\u{00FE}", "yuml": "\u{00FF}",
|
||||
// Special: markup-significant, quotes, dashes, and spacing.
|
||||
"quot": "\u{0022}", "amp": "\u{0026}", "apos": "\u{0027}", "lt": "\u{003C}", "gt": "\u{003E}",
|
||||
"OElig": "\u{0152}", "oelig": "\u{0153}", "Scaron": "\u{0160}", "scaron": "\u{0161}",
|
||||
"Yuml": "\u{0178}", "circ": "\u{02C6}", "tilde": "\u{02DC}", "ensp": "\u{2002}",
|
||||
"emsp": "\u{2003}", "thinsp": "\u{2009}", "zwnj": "\u{200C}", "zwj": "\u{200D}",
|
||||
"lrm": "\u{200E}", "rlm": "\u{200F}", "ndash": "\u{2013}", "mdash": "\u{2014}",
|
||||
"lsquo": "\u{2018}", "rsquo": "\u{2019}", "sbquo": "\u{201A}", "ldquo": "\u{201C}",
|
||||
"rdquo": "\u{201D}", "bdquo": "\u{201E}", "dagger": "\u{2020}", "Dagger": "\u{2021}",
|
||||
"permil": "\u{2030}", "lsaquo": "\u{2039}", "rsaquo": "\u{203A}", "euro": "\u{20AC}",
|
||||
// Symbols: Greek letters, mathematical operators, arrows, and card suits.
|
||||
"fnof": "\u{0192}", "Alpha": "\u{0391}", "Beta": "\u{0392}", "Gamma": "\u{0393}",
|
||||
"Delta": "\u{0394}", "Epsilon": "\u{0395}", "Zeta": "\u{0396}", "Eta": "\u{0397}",
|
||||
"Theta": "\u{0398}", "Iota": "\u{0399}", "Kappa": "\u{039A}", "Lambda": "\u{039B}",
|
||||
"Mu": "\u{039C}", "Nu": "\u{039D}", "Xi": "\u{039E}", "Omicron": "\u{039F}",
|
||||
"Pi": "\u{03A0}", "Rho": "\u{03A1}", "Sigma": "\u{03A3}", "Tau": "\u{03A4}",
|
||||
"Upsilon": "\u{03A5}", "Phi": "\u{03A6}", "Chi": "\u{03A7}", "Psi": "\u{03A8}",
|
||||
"Omega": "\u{03A9}", "alpha": "\u{03B1}", "beta": "\u{03B2}", "gamma": "\u{03B3}",
|
||||
"delta": "\u{03B4}", "epsilon": "\u{03B5}", "zeta": "\u{03B6}", "eta": "\u{03B7}",
|
||||
"theta": "\u{03B8}", "iota": "\u{03B9}", "kappa": "\u{03BA}", "lambda": "\u{03BB}",
|
||||
"mu": "\u{03BC}", "nu": "\u{03BD}", "xi": "\u{03BE}", "omicron": "\u{03BF}",
|
||||
"pi": "\u{03C0}", "rho": "\u{03C1}", "sigmaf": "\u{03C2}", "sigma": "\u{03C3}",
|
||||
"tau": "\u{03C4}", "upsilon": "\u{03C5}", "phi": "\u{03C6}", "chi": "\u{03C7}",
|
||||
"psi": "\u{03C8}", "omega": "\u{03C9}", "thetasym": "\u{03D1}", "upsih": "\u{03D2}",
|
||||
"piv": "\u{03D6}", "bull": "\u{2022}", "hellip": "\u{2026}", "prime": "\u{2032}",
|
||||
"Prime": "\u{2033}", "oline": "\u{203E}", "frasl": "\u{2044}", "weierp": "\u{2118}",
|
||||
"image": "\u{2111}", "real": "\u{211C}", "trade": "\u{2122}", "alefsym": "\u{2135}",
|
||||
"larr": "\u{2190}", "uarr": "\u{2191}", "rarr": "\u{2192}", "darr": "\u{2193}",
|
||||
"harr": "\u{2194}", "crarr": "\u{21B5}", "lArr": "\u{21D0}", "uArr": "\u{21D1}",
|
||||
"rArr": "\u{21D2}", "dArr": "\u{21D3}", "hArr": "\u{21D4}", "forall": "\u{2200}",
|
||||
"part": "\u{2202}", "exist": "\u{2203}", "empty": "\u{2205}", "nabla": "\u{2207}",
|
||||
"isin": "\u{2208}", "notin": "\u{2209}", "ni": "\u{220B}", "prod": "\u{220F}",
|
||||
"sum": "\u{2211}", "minus": "\u{2212}", "lowast": "\u{2217}", "radic": "\u{221A}",
|
||||
"prop": "\u{221D}", "infin": "\u{221E}", "ang": "\u{2220}", "and": "\u{2227}",
|
||||
"or": "\u{2228}", "cap": "\u{2229}", "cup": "\u{222A}", "int": "\u{222B}",
|
||||
"there4": "\u{2234}", "sim": "\u{223C}", "cong": "\u{2245}", "asymp": "\u{2248}",
|
||||
"ne": "\u{2260}", "equiv": "\u{2261}", "le": "\u{2264}", "ge": "\u{2265}",
|
||||
"sub": "\u{2282}", "sup": "\u{2283}", "nsub": "\u{2284}", "sube": "\u{2286}",
|
||||
"supe": "\u{2287}", "oplus": "\u{2295}", "otimes": "\u{2297}", "perp": "\u{22A5}",
|
||||
"sdot": "\u{22C5}", "lceil": "\u{2308}", "rceil": "\u{2309}", "lfloor": "\u{230A}",
|
||||
"rfloor": "\u{230B}", "lang": "\u{2329}", "rang": "\u{232A}", "loz": "\u{25CA}",
|
||||
"spades": "\u{2660}", "clubs": "\u{2663}", "hearts": "\u{2665}", "diams": "\u{2666}",
|
||||
]
|
||||
}
|
||||
@ -50,4 +50,15 @@ struct HTMLTokenizerTests {
|
||||
#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
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user