extension PureXML.XInclude { /// A parsed URI reference (scheme, authority, path, query, fragment) and the /// RFC 3986 reference-resolution algorithm. No I/O: it only manipulates the /// identifier strings so a relative `href` can be resolved against a base. struct URIReference: Equatable { var scheme: String? var authority: String? var path: String var query: String? var fragment: String? /// Resolves `reference` against `base`, returning the resolved URI string. static func resolve(_ reference: String, against base: String) -> String { let ref = parse(reference) let baseRef = parse(base) return ref.resolved(against: baseRef).recomposed() } /// The RFC 3986 §5.2.2 transform. func resolved(against base: URIReference) -> URIReference { if scheme != nil { return URIReference( scheme: scheme, authority: authority, path: Self.removeDotSegments(path), query: query, fragment: fragment, ) } var result = URIReference(scheme: base.scheme, authority: nil, path: "", query: nil, fragment: fragment) if let authority { result.authority = authority result.path = Self.removeDotSegments(path) result.query = query return result } result.authority = base.authority resolvePath(base: base, into: &result) return result } private func resolvePath(base: URIReference, into result: inout URIReference) { if path.isEmpty { result.path = base.path result.query = query ?? base.query } else { let merged = path.hasPrefix("/") ? path : Self.merge(base: base, reference: path) result.path = Self.removeDotSegments(merged) result.query = query } } private static func merge(base: URIReference, reference: String) -> String { if base.authority != nil, base.path.isEmpty { return "/" + reference } guard let slash = base.path.lastIndex(of: "/") else { return reference } return base.path[...slash] + reference } /// The RFC 3986 §5.2.4 dot-segment removal. static func removeDotSegments(_ path: String) -> String { var input = Substring(path) var output = "" while !input.isEmpty { if input.hasPrefix("../") { input = input.dropFirst(3) } else if input.hasPrefix("./") { input = input.dropFirst(2) } else if input.hasPrefix("/./") { input = "/" + input.dropFirst(3) } else if input == "/." { input = "/" } else if input.hasPrefix("/../") { input = "/" + input.dropFirst(4) dropLastSegment(&output) } else if input == "/.." { input = "/" dropLastSegment(&output) } else if input == "." || input == ".." { input = "" } else { moveSegment(&input, &output) } } return output } private static func dropLastSegment(_ output: inout String) { if let slash = output.lastIndex(of: "/") { output = String(output[.. String { var result = "" if let scheme { result += scheme + ":" } if let authority { result += "//" + authority } result += path if let query { result += "?" + query } if let fragment { result += "#" + fragment } return result } static func parse(_ string: String) -> URIReference { var rest = Substring(string) var reference = URIReference(scheme: nil, authority: nil, path: "", query: nil, fragment: nil) if let hash = rest.firstIndex(of: "#") { reference.fragment = String(rest[rest.index(after: hash)...]) rest = rest[.. String? { guard let colon = rest.firstIndex(of: ":") else { return nil } let candidate = rest[..