feat(html): HTML 4 void elements and the ampersand-brace convention

basefont and isindex join the void-element set, and the html output
method leaves an ampersand unescaped when a brace follows (the
&{...} attribute convention of XSLT 16.2). Xalan baseline 134 -> 131
(output 20 -> 17).
This commit is contained in:
Mihaela Mihaljevic 2026-06-10 16:41:54 +02:00
parent 6174c7bde1
commit 06a7a9fcbd
4 changed files with 14 additions and 6 deletions

View File

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- HTML 4 serialization corners, sixteenth xalan burn-down (#130): `basefont` and `isindex` join the void-element set (no end tags), and the html method leaves an ampersand unescaped when a brace follows, the historical `&{...}` attribute convention of 16.2. Baseline 134 to 131 (output 20 to 17).
- Globals, prefix wildcards, fifteenth xalan burn-down (#130): a top-level variable now evaluates with every previously evaluated global in scope, so a variable whose body references an earlier top-level param (directly or via call-template) resolves; same-name globals resolve by import precedence (folds happen in precedence order and the last same-name entry wins, so a later sibling import beats an earlier one and the importing sheet beats both); the stylesheet fold uniformly favors later contributions for attribute-set definitions, decimal formats, namespace aliases, and output settings, matching the post-order precedence model; and the XPath compiler accepts the `NCName:*` name-test form, matching any name in the prefix's namespace (bound by URI, falling back to the in-document prefix string) at its -0.25 default priority, so `@ped:*` outranks `@*`. Three unit tests pin the behaviors. Baseline 144 to 134 (variable 10 to 5, conflictres 8 to 5).
- Import precedence and relative composition, fourteenth xalan burn-down (#130): import precedences are assigned in post-order over the import tree, so sibling imports are distinct (later higher) and every unit sits above its own imports; apply-imports searches only the current template's stylesheet's own import subtree per 5.6 (templates carry their unit's precedence range), falling back to the built-in rule when that unit imports nothing; an included stylesheet's declarations join the including unit at its precedence while its imports join the import list; and include/import hrefs resolve against the including stylesheet's own URI, so nested relative chains load correctly. Also fixed along the way: an included document's tree was only weakly referenced once flattened, so its ancestor chain (xmlns declarations, exclusions in scope) could disappear before its templates were parsed. Two unit tests pin apply-imports scoping and relative chains. Baseline 159 to 144 (reluri 11 to 3, impincl 9 to 2).
- Attribute sets, copied namespaces, html booleans, loader entities, thirteenth xalan burn-down (#130): same-name `xsl:attribute-set` declarations merge as ordered definitions per 7.1.4 (each expands its used sets before its own attributes, a later definition's attributes winning); `xsl:copy` of an element carries the source element's in-scope namespace nodes per 7.5 (the fixup pass drops result-redundant ones); the namespace fixup lets a carried attribute prefix shadow an inherited binding locally (matching the source document's shape) unless the element's own name relies on it; the html method minimizes boolean attributes whose value repeats their name (CHECKED="CHECKED" serializes as CHECKED); and external DTDs and entities referenced by the stylesheet or source now resolve through the transform's document loader, the same channel as document(). Four unit tests pin the new behaviors. Baseline 174 to 159 (attribset 13 to 4, copy 12 to 7).

View File

@ -73,14 +73,23 @@ extension PureXML.HTML {
private static func escapeAttribute(_ value: String) -> String {
var result = ""
var previousWasAmpersand = false
for scalar in value.unicodeScalars {
if previousWasAmpersand {
// The html method leaves & unescaped when a { follows
// (the &{...} convention, 16.2).
result += scalar == "{" ? "&" : "&"
previousWasAmpersand = false
}
switch scalar {
case "&": result += "&"
case "&":
previousWasAmpersand = true
case "\"": result += """
case "<": result += "&lt;"
default: result += latin1Entity(scalar) ?? String(Character(scalar))
}
}
if previousWasAmpersand { result += "&amp;" }
return result
}

View File

@ -27,8 +27,9 @@ extension PureXML.HTML {
enum Elements {
/// Void elements: no content and no end tag.
static let void: Set<String> = [
"area", "base", "br", "col", "embed", "frame", "hr", "img", "input",
"link", "meta", "param", "source", "track", "wbr",
"area", "base", "basefont", "br", "col", "embed", "frame", "hr",
"img", "input", "isindex", "link", "meta", "param", "source",
"track", "wbr",
]
/// Raw-text elements: their content is taken verbatim up to the matching

View File

@ -82,9 +82,6 @@ numbering/numbering63
output/output22
output/output26
output/output32
output/output33
output/output34
output/output37
output/output40
output/output48
output/output49