diff --git a/CHANGELOG.md b/CHANGELOG.md index bf8e273..fcb1bd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/Sources/HTML/HTMLSerializer.swift b/Sources/HTML/HTMLSerializer.swift index 96ee3db..061dd67 100644 --- a/Sources/HTML/HTMLSerializer.swift +++ b/Sources/HTML/HTMLSerializer.swift @@ -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 += "<" default: result += latin1Entity(scalar) ?? String(Character(scalar)) } } + if previousWasAmpersand { result += "&" } return result } diff --git a/Sources/HTML/HTMLToken.swift b/Sources/HTML/HTMLToken.swift index 712f1e0..065f425 100644 --- a/Sources/HTML/HTMLToken.swift +++ b/Sources/HTML/HTMLToken.swift @@ -27,8 +27,9 @@ extension PureXML.HTML { enum Elements { /// Void elements: no content and no end tag. static let void: Set = [ - "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 diff --git a/Tests/Fixtures/xalan-baseline.txt b/Tests/Fixtures/xalan-baseline.txt index bb31aec..d250b1c 100644 --- a/Tests/Fixtures/xalan-baseline.txt +++ b/Tests/Fixtures/xalan-baseline.txt @@ -82,9 +82,6 @@ numbering/numbering63 output/output22 output/output26 output/output32 -output/output33 -output/output34 -output/output37 output/output40 output/output48 output/output49