Skip to content

Commit 03b711a

Browse files
committed
Optimize url readable path and fragment logic
Because `urlReadablePath` was always call in the topic reference's initializer, when adding and removing path components, we were performing duplicate work. This adds a new private initializer that skips the `urlReadablePath` call when we know we already have an escaped path.
1 parent 57960f1 commit 03b711a

File tree

2 files changed

+51
-21
lines changed

2 files changed

+51
-21
lines changed

Sources/SwiftDocC/Model/Identifier.swift

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,22 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
140140
}
141141

142142
public init(bundleIdentifier: String, path: String, fragment: String? = nil, sourceLanguages: Set<SourceLanguage>) {
143+
self.init(
144+
bundleIdentifier: bundleIdentifier,
145+
urlReadablePath: urlReadablePath(path),
146+
urlReadableFragment: fragment.map(urlReadableFragment(_:)),
147+
sourceLanguages: sourceLanguages
148+
)
149+
}
150+
151+
private init(bundleIdentifier: String, urlReadablePath: String, urlReadableFragment: String? = nil, sourceLanguages: Set<SourceLanguage>) {
143152
precondition(!sourceLanguages.isEmpty, "ResolvedTopicReference.sourceLanguages cannot be empty")
144153
// Check for a cached instance of the reference
145-
let key = Self.cacheKey(path: path, fragment: fragment, sourceLanguages: sourceLanguages)
154+
let key = Self.cacheKey(
155+
urlReadablePath: urlReadablePath,
156+
urlReadableFragment: urlReadableFragment,
157+
sourceLanguages: sourceLanguages
158+
)
146159
let cached = Self.sharedPool.sync { $0[bundleIdentifier]?[key] }
147160
if let resolved = cached {
148161
self = resolved
@@ -252,7 +265,7 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
252265
public func appendingPath(_ path: String) -> ResolvedTopicReference {
253266
let newReference = ResolvedTopicReference(
254267
bundleIdentifier: bundleIdentifier,
255-
path: url.appendingPathComponent(urlReadablePath(path), isDirectory: false).path,
268+
urlReadablePath: url.appendingPathComponent(urlReadablePath(path), isDirectory: false).path,
256269
sourceLanguages: sourceLanguages
257270
)
258271
return newReference
@@ -273,8 +286,8 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
273286
let newPath = url.appendingPathComponent(referencePath, isDirectory: false).path
274287
let newReference = ResolvedTopicReference(
275288
bundleIdentifier: bundleIdentifier,
276-
path: newPath,
277-
fragment: reference.fragment,
289+
urlReadablePath: newPath,
290+
urlReadableFragment: reference.fragment,
278291
sourceLanguages: sourceLanguages
279292
)
280293
return newReference
@@ -285,8 +298,8 @@ public struct ResolvedTopicReference: Hashable, Codable, Equatable, CustomString
285298
let newPath = String(pathComponents.dropLast().joined(separator: "/").dropFirst())
286299
let newReference = ResolvedTopicReference(
287300
bundleIdentifier: bundleIdentifier,
288-
path: newPath,
289-
fragment: fragment,
301+
urlReadablePath: newPath,
302+
urlReadableFragment: fragment,
290303
sourceLanguages: sourceLanguages
291304
)
292305
return newReference
@@ -514,32 +527,30 @@ public struct ResourceReference: Hashable {
514527
/// For example, a path like `"hello world/example project"` is converted to `"hello-world/example-project"`
515528
/// instead of `"hello%20world/example%20project"`.
516529
func urlReadablePath(_ path: String) -> String {
517-
return path.components(separatedBy: CharacterSet.urlPathAllowed.inverted)
518-
.joined(separator: "-")
530+
return path.components(separatedBy: .urlPathNotAllowed).joined(separator: "-")
531+
}
532+
533+
private extension CharacterSet {
534+
static let invalidCharacterSet = CharacterSet(charactersIn: "'\"`")
535+
static let whitespaceAndDashes = CharacterSet(charactersIn: "-").union(.whitespaces)
519536
}
520537

521538
/// Creates a more readable version of a fragment by replacing characters that are not allowed in the fragment of a URL with hyphens.
522539
///
523540
/// If this step is not performed, the disallowed characters are instead percent escape encoded, which is less readable.
524541
/// For example, a fragment like `"#hello world"` is converted to `"#hello-world"` instead of `"#hello%20world"`.
525542
func urlReadableFragment(_ fragment: String) -> String {
526-
// Trim leading/trailing invalid characters
527543
var fragment = fragment
544+
// Trim leading/trailing whitespace
528545
.trimmingCharacters(in: .whitespaces)
529546

530-
// Replace continuous whitespace
531-
fragment = fragment.components(separatedBy: .whitespaces)
547+
// Replace continuous whitespace and dashes
548+
.components(separatedBy: .whitespaceAndDashes)
532549
.filter({ !$0.isEmpty })
533550
.joined(separator: "-")
534-
535-
let invalidCharacterSet = CharacterSet(charactersIn: "'\"`")
536-
fragment = fragment.components(separatedBy: invalidCharacterSet)
537-
.joined()
538-
539-
// Replace continuous dashes
540-
fragment = fragment.components(separatedBy: CharacterSet(charactersIn: "-"))
541-
.filter({ !$0.isEmpty })
542-
.joined(separator: "-")
543-
551+
552+
// Remove invalid characters
553+
fragment.unicodeScalars.removeAll(where: CharacterSet.invalidCharacterSet.contains)
554+
544555
return fragment
545556
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
extension CharacterSet {
14+
/// Returns the character set for characters **not** allowed in a path URL component.
15+
static let urlPathNotAllowed = CharacterSet.urlPathAllowed.inverted
16+
17+
/// Returns the union of the `whitespaces` and `punctuationCharacters` character sets.
18+
static let whitespacesAndPunctuation = CharacterSet.whitespaces.union(.punctuationCharacters)
19+
}

0 commit comments

Comments
 (0)