diff --git a/CHANGELOG.md b/CHANGELOG.md index b4a0710..b51eac4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Five unused internal schema-consistency wrappers (`idAttributeErrors`, `consistencyErrors`, `postNamedTypeErrors`, `structureErrors`, `includeCompositionErrors`) that returned `[String]` reason lists, along with the now-orphaned `postCompileErrors(schema:context:containers:namedTypes:)` overload and the `SchemaCompileNamedTypes` struct it consumed. None had callers; the `XSDParser.consistencyErrors` overload was shadowed by the distinct, still-live `Validation.XSDSchema.consistencyErrors` (a different signature returning `ValidationError`), which is why a name-only search first read it as used. The located-diagnostic API (the `*Findings` functions returning `SchemaLocatedFinding`, and `Validation.XSDSchema` returning `ValidationError`) superseded all of them. Removing them deletes dead code with no behavior change; the live `*Findings` and `Validation.XSDSchema` functions are unchanged. A stale doc-comment reference to the renamed `userTypeValueConstraintErrors` (now `userTypeValueConstraintFindings`) was corrected at the same time. +- Two unused private helpers in `XSLTNamespaceFixup` (`fixAttributeName` and `resolvedPrefix`). The result-tree attribute fixup they implemented was inlined into `fix(_ element:)` and evolved past them (the inlined version additionally shadows a carried prefix through `localPrefixes`, which the helpers did not), leaving both with zero callers. Removing them deletes dead, diverged duplicate logic with no behavior change; the live inlined path is unchanged. + ### Fixed - `xpointer(string-range(...))` no longer uses a quadratic substring search. It matched the search string at every position of each node's value and re-allocated the whole needle-width window per position (O(text * needle), a denial-of-service vector on a long node value with a near-matching needle); it now uses a linear-time Knuth-Morris-Pratt search with the failure function built once. The matches are unchanged: same non-overlapping, left-to-right occurrences, same offset/length. A near-matching needle over a 16000-character node drops from about 0.18 s to about 0.002 s (~100x), and the cost is now linear. Guarded by the new non-overlapping and backtracking cases in `XPointerRangeTests`. diff --git a/Sources/XSLT/XSLTNamespaceFixup.swift b/Sources/XSLT/XSLTNamespaceFixup.swift index f37bb7a..7b2d343 100644 --- a/Sources/XSLT/XSLTNamespaceFixup.swift +++ b/Sources/XSLT/XSLTNamespaceFixup.swift @@ -34,54 +34,6 @@ enum XSLTNamespaceFixup { } } - /// A namespaced attribute needs a prefix bound to its URI, generated - /// fresh when absent or taken by a different binding. - private static func fixAttributeName(at index: Int, attributes: inout [PureXML.Model.Attribute], scope: inout [String: String], counter: inout Int) { - let attributeName = attributes[index].name - guard let uri = attributeName.namespaceURI, !uri.isEmpty, - attributeName.prefix != "xmlns", attributeName.prefix != "xml" - else { return } - var prefix = attributeName.prefix - if let candidate = prefix, scope[candidate] == uri { - return // Already bound correctly. - } - if prefix == nil || scope[prefix ?? ""] != nil { - if let existing = scope.first(where: { $0.value == uri && !$0.key.isEmpty })?.key { - prefix = existing - } else { - while scope["ns\(counter)"] != nil { - counter += 1 - } - prefix = "ns\(counter)" - counter += 1 - } - } - guard let resolved = prefix else { return } - if scope[resolved] != uri { - scope[resolved] = uri - attributes.append(.init("xmlns:\(resolved)", uri)) - } - attributes[index] = .init( - name: .init(prefix: resolved, localName: attributeName.localName, namespaceURI: uri), - value: attributes[index].value, - ) - } - - /// A prefix bound to `uri`: the carried one when free, an existing - /// binding for the same URI, or a fresh `ns`. - private static func resolvedPrefix(for uri: String, carried: String?, scope: [String: String], counter: inout Int) -> String { - if let carried, scope[carried] == nil { return carried } - if let existing = scope.first(where: { $0.value == uri && !$0.key.isEmpty })?.key { - return existing - } - var fresh = counter - while scope["ns\(fresh)"] != nil { - fresh += 1 - } - counter = fresh + 1 - return "ns\(fresh)" - } - private static func fix(_ node: PureXML.Model.Node, inScope: [String: String], counter: inout Int) -> PureXML.Model.Node { switch node { case let .document(children):