Skip to content

Commit 3374687

Browse files
authored
Fix to auto-capitalization PR (#888)
rdar://122167705 * Changed withFirstWordCapitalized from a computed property to a function. * Remove unused protocol and extension, and extracted repeated capitalization logic into [RenderBlockContent] and [RenderInlineContent] extensions. * Rename function from capitalizeFirstWord() to capitalizingFirstWord() since this is a non-mutating function.
1 parent 54fc89b commit 3374687

File tree

9 files changed

+81
-102
lines changed

9 files changed

+81
-102
lines changed

Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent+Capitalization.swift

Lines changed: 36 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -8,85 +8,73 @@
88
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
*/
1010

11-
/// For auto capitalizing the first letter of a sentence following a colon (e.g. asides, sections such as parameters, returns).
12-
protocol AutoCapitalizable {
13-
14-
/// Any type that conforms to the AutoCapitalizable protocol will have the first letter of the first word capitalized (if applicable).
15-
var withFirstWordCapitalized: Self {
16-
get
17-
}
18-
19-
}
20-
21-
extension AutoCapitalizable {
22-
var withFirstWordCapitalized: Self { return self }
23-
}
2411

25-
extension RenderInlineContent: AutoCapitalizable {
12+
extension RenderInlineContent {
2613
/// Capitalize the first word for normal text content, as well as content that has emphasis or strong applied.
27-
var withFirstWordCapitalized: Self {
14+
func capitalizingFirstWord() -> Self {
2815
switch self {
2916
case .text(let text):
30-
return .text(text.capitalizeFirstWord())
17+
return .text(text.capitalizingFirstWord())
3118
case .emphasis(inlineContent: let embeddedContent):
32-
return .emphasis(inlineContent: [embeddedContent[0].withFirstWordCapitalized] + embeddedContent[1...])
19+
return .emphasis(inlineContent: embeddedContent.capitalizingFirstWord())
3320
case .strong(inlineContent: let embeddedContent):
34-
return .strong(inlineContent: [embeddedContent[0].withFirstWordCapitalized] + embeddedContent[1...])
21+
return .strong(inlineContent: embeddedContent.capitalizingFirstWord())
3522
default:
3623
return self
3724
}
3825
}
3926
}
4027

28+
extension [RenderBlockContent] {
29+
func capitalizingFirstWord() -> Self {
30+
guard let first else { return [] }
31+
32+
return [first.capitalizingFirstWord()] + dropFirst()
33+
}
34+
}
35+
36+
extension [RenderInlineContent] {
37+
func capitalizingFirstWord() -> Self {
38+
guard let first else { return [] }
39+
40+
return [first.capitalizingFirstWord()] + dropFirst()
41+
}
42+
}
4143

42-
extension RenderBlockContent: AutoCapitalizable {
44+
45+
extension RenderBlockContent {
4346
/// Capitalize the first word for paragraphs, asides, headings, and small content.
44-
var withFirstWordCapitalized: Self {
47+
func capitalizingFirstWord() -> Self {
4548
switch self {
4649
case .paragraph(let paragraph):
47-
return .paragraph(paragraph.withFirstWordCapitalized)
50+
return .paragraph(paragraph.capitalizingFirstWord())
4851
case .aside(let aside):
49-
return .aside(aside.withFirstWordCapitalized)
52+
return .aside(aside.capitalizingFirstWord())
5053
case .small(let small):
51-
return .small(small.withFirstWordCapitalized)
54+
return .small(small.capitalizingFirstWord())
5255
case .heading(let heading):
53-
return .heading(.init(level: heading.level, text: heading.text.capitalizeFirstWord(), anchor: heading.anchor))
56+
return .heading(.init(level: heading.level, text: heading.text.capitalizingFirstWord(), anchor: heading.anchor))
5457
default:
5558
return self
5659
}
5760
}
5861
}
5962

60-
extension RenderBlockContent.Paragraph: AutoCapitalizable {
61-
var withFirstWordCapitalized: RenderBlockContent.Paragraph {
62-
guard !self.inlineContent.isEmpty else {
63-
return self
64-
}
65-
66-
let inlineContent = [self.inlineContent[0].withFirstWordCapitalized] + self.inlineContent[1...]
67-
return .init(inlineContent: inlineContent)
63+
extension RenderBlockContent.Paragraph {
64+
func capitalizingFirstWord() -> RenderBlockContent.Paragraph {
65+
return .init(inlineContent: inlineContent.capitalizingFirstWord())
6866
}
6967
}
7068

71-
extension RenderBlockContent.Aside: AutoCapitalizable {
72-
var withFirstWordCapitalized: RenderBlockContent.Aside {
73-
guard !self.content.isEmpty else {
74-
return self
75-
}
76-
77-
let content = [self.content[0].withFirstWordCapitalized] + self.content[1...]
78-
return .init(style: self.style, content: content)
69+
extension RenderBlockContent.Aside {
70+
func capitalizingFirstWord() -> RenderBlockContent.Aside {
71+
return .init(style: self.style, content: self.content.capitalizingFirstWord())
7972
}
8073
}
8174

82-
extension RenderBlockContent.Small: AutoCapitalizable {
83-
var withFirstWordCapitalized: RenderBlockContent.Small {
84-
guard !self.inlineContent.isEmpty else {
85-
return self
86-
}
87-
88-
let inlineContent = [self.inlineContent[0].withFirstWordCapitalized] + self.inlineContent[1...]
89-
return .init(inlineContent: inlineContent)
75+
extension RenderBlockContent.Small {
76+
func capitalizingFirstWord() -> RenderBlockContent.Small {
77+
return .init(inlineContent: self.inlineContent.capitalizingFirstWord())
9078
}
9179
}
9280

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ struct RenderContentCompiler: MarkupVisitor {
4242
content: aside.content.reduce(into: [], { result, child in result.append(contentsOf: visit(child))}) as! [RenderBlockContent]
4343
)
4444

45-
return [RenderBlockContent.aside(newAside.withFirstWordCapitalized)]
45+
return [RenderBlockContent.aside(newAside.capitalizingFirstWord())]
4646
}
4747

4848
mutating func visitCodeBlock(_ codeBlock: CodeBlock) -> [RenderContent] {

Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DiscussionSectionTranslator.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ struct DiscussionSectionTranslator: RenderSectionTranslator {
2727
return nil
2828
}
2929

30-
let capitalizedDiscussionContent = [discussionContent[0].withFirstWordCapitalized] + discussionContent[1...]
31-
3230
let title: String?
3331
if let first = discussionContent.first, case RenderBlockContent.heading = first {
3432
// There's already an authored heading. Don't add another heading.
@@ -44,7 +42,7 @@ struct DiscussionSectionTranslator: RenderSectionTranslator {
4442
}
4543
}
4644

47-
return ContentRenderSection(kind: .content, content: capitalizedDiscussionContent, heading: title)
45+
return ContentRenderSection(kind: .content, content: discussionContent.capitalizingFirstWord(), heading: title)
4846
}
4947
}
5048
}

Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/ParametersSectionTranslator.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ struct ParametersSectionTranslator: RenderSectionTranslator {
3333
return ParameterRenderSection(name: parameter.name, content: parameterContent)
3434
}
3535

36-
let capitalizedParameterContent = [parameterContent[0].withFirstWordCapitalized] + parameterContent[1...]
37-
38-
return ParameterRenderSection(name: parameter.name, content: capitalizedParameterContent)
36+
return ParameterRenderSection(name: parameter.name, content: parameterContent.capitalizingFirstWord())
3937
}
4038
)
4139
}

Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/ReturnsSectionTranslator.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ struct ReturnsSectionTranslator: RenderSectionTranslator {
2828
return nil
2929
}
3030

31-
let capitalizedReturnsContent = [returnsContent[0].withFirstWordCapitalized] + returnsContent[1...]
32-
33-
return ContentRenderSection(kind: .content, content: capitalizedReturnsContent, heading: "Return Value")
31+
return ContentRenderSection(kind: .content, content: returnsContent.capitalizingFirstWord(), heading: "Return Value")
3432
}
3533
}
3634
}

Sources/SwiftDocC/Utility/FoundationExtensions/String+Capitalization.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,23 @@ import Foundation
1212

1313
extension String {
1414

15-
// Precomputes the CharacterSet to use in capitalizeFirstWord().
15+
// Precomputes the CharacterSet to use in capitalizingFirstWord().
1616
private static let charactersPreventingWordCapitalization = CharacterSet.lowercaseLetters.union(.punctuationCharacters).inverted
1717

1818
/// Returns the string with the first letter capitalized.
1919
/// This auto-capitalization only occurs if the first word is all lowercase and contains only lowercase letters.
2020
/// The first word can also contain punctuation (e.g. a period, comma, hyphen, semi-colon, colon).
21-
func capitalizeFirstWord() -> String {
21+
func capitalizingFirstWord() -> String {
2222
guard let firstWordStartIndex = self.firstIndex(where: { !$0.isWhitespace && !$0.isNewline }) else { return self }
2323
let firstWord = self[firstWordStartIndex...].prefix(while: { !$0.isWhitespace && !$0.isNewline})
2424

2525
guard firstWord.rangeOfCharacter(from: Self.charactersPreventingWordCapitalization) == nil else {
2626
return self
2727
}
2828

29-
var resultString = String()
30-
resultString.reserveCapacity(self.count)
31-
resultString.append(contentsOf: self[..<firstWordStartIndex])
32-
resultString.append(contentsOf: String(firstWord).localizedCapitalized)
33-
let restStartIndex = self.index(firstWordStartIndex, offsetBy: firstWord.count)
34-
resultString.append(contentsOf: self[restStartIndex...])
29+
var resultString = self
30+
31+
resultString.replaceSubrange(firstWordStartIndex..<firstWord.endIndex, with: firstWord.localizedCapitalized)
3532

3633
return resultString
3734
}

Tests/SwiftDocCTests/Infrastructure/AutoCapitalizationTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class AutoCapitalizationTests: XCTestCase {
100100
XCTAssertEqual(context.problems.count, 0)
101101

102102
let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/ModuleName/functionName(...)", sourceLanguage: .swift)
103-
var node = try context.entity(with: reference)
103+
let node = try context.entity(with: reference)
104104
let symbol = try XCTUnwrap(node.semantic as? Symbol)
105105
let parameterSections = symbol.parametersSectionVariants
106106
XCTAssertEqual(parameterSections[.swift]?.parameters.map(\.name), ["one", "two", "three", "four", "five"])

Tests/SwiftDocCTests/Model/RenderBlockContent+CapitalizationTests.swift

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,75 +19,75 @@ class RenderBlockContent_CapitalizationTests: XCTestCase {
1919
// Text, Emphasis, Strong are all auto-capitalized, and everything else defaults to not capitalized.
2020

2121
func testRenderInlineContentText() {
22-
let text = RenderInlineContent.text("hello, world!").withFirstWordCapitalized
22+
let text = RenderInlineContent.text("hello, world!").capitalizingFirstWord()
2323
XCTAssertEqual("Hello, world!", text.plainText)
2424
}
2525

2626
func testRenderInlineContentEmphasis() {
27-
let emphasis = RenderInlineContent.emphasis(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
27+
let emphasis = RenderInlineContent.emphasis(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
2828
XCTAssertEqual("Hello, world!", emphasis.plainText)
2929
}
3030

3131
func testRenderInlineContentStrong() {
32-
let strong = RenderInlineContent.strong(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
32+
let strong = RenderInlineContent.strong(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
3333
XCTAssertEqual("Hello, world!", strong.plainText)
3434
}
3535

3636
func testRenderInlineContentCodeVoice() {
37-
let codeVoice = RenderInlineContent.codeVoice(code: "code voice").withFirstWordCapitalized
37+
let codeVoice = RenderInlineContent.codeVoice(code: "code voice").capitalizingFirstWord()
3838
XCTAssertEqual("code voice", codeVoice.plainText)
3939
}
4040

4141
func testRenderInlineContentReference() {
42-
let reference = RenderInlineContent.reference(identifier: .init("Test"), isActive: true, overridingTitle: "hello, world!", overridingTitleInlineContent: [.text("hello, world!")]).withFirstWordCapitalized
42+
let reference = RenderInlineContent.reference(identifier: .init("Test"), isActive: true, overridingTitle: "hello, world!", overridingTitleInlineContent: [.text("hello, world!")]).capitalizingFirstWord()
4343
XCTAssertEqual("hello, world!", reference.plainText)
4444
}
4545

4646
func testRenderInlineContentNewTerm() {
47-
let newTerm = RenderInlineContent.newTerm(inlineContent: [.text("helloWorld")]).withFirstWordCapitalized
47+
let newTerm = RenderInlineContent.newTerm(inlineContent: [.text("helloWorld")]).capitalizingFirstWord()
4848
XCTAssertEqual("helloWorld", newTerm.plainText)
4949
}
5050

5151
func testRenderInlineContentInlineHead() {
52-
let inlineHead = RenderInlineContent.inlineHead(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
52+
let inlineHead = RenderInlineContent.inlineHead(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
5353
XCTAssertEqual("hello, world!", inlineHead.plainText)
5454
}
5555

5656
func testRenderInlineContentSubscript() {
57-
let subscriptContent = RenderInlineContent.subscript(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
57+
let subscriptContent = RenderInlineContent.subscript(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
5858
XCTAssertEqual("hello, world!", subscriptContent.plainText)
5959
}
6060

6161
func testRenderInlineContentSuperscript() {
62-
let superscriptContent = RenderInlineContent.superscript(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
62+
let superscriptContent = RenderInlineContent.superscript(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
6363
XCTAssertEqual("hello, world!", superscriptContent.plainText)
6464
}
6565

6666
func testRenderInlineContentStrikethrough() {
67-
let strikethrough = RenderInlineContent.strikethrough(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
67+
let strikethrough = RenderInlineContent.strikethrough(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
6868
XCTAssertEqual("hello, world!", strikethrough.plainText)
6969
}
7070

7171
// MARK: - Blocks
7272
// Paragraphs, asides, headings, and small content are all auto-capitalized, and everything else defaults to not capitalized.
7373

7474
func testRenderBlockContentParagraph() {
75-
let paragraph = RenderBlockContent.paragraph(.init(inlineContent: [.text("hello, world!")])).withFirstWordCapitalized
75+
let paragraph = RenderBlockContent.paragraph(.init(inlineContent: [.text("hello, world!")])).capitalizingFirstWord()
7676
XCTAssertEqual("Hello, world!", paragraph.rawIndexableTextContent(references: [:]))
7777
}
7878

7979
func testRenderBlockContentAside() {
80-
let aside = RenderBlockContent.aside(.init(style: .init(rawValue: "Experiment"), content: [.paragraph(.init(inlineContent: [.text("hello, world!")]))])).withFirstWordCapitalized
80+
let aside = RenderBlockContent.aside(.init(style: .init(rawValue: "Experiment"), content: [.paragraph(.init(inlineContent: [.text("hello, world!")]))])).capitalizingFirstWord()
8181
XCTAssertEqual("Hello, world!", aside.rawIndexableTextContent(references: [:]))
8282
}
8383

8484
func testRenderBlockContentSmall() {
85-
let small = RenderBlockContent.small(.init(inlineContent: [.text("hello, world!")])).withFirstWordCapitalized
85+
let small = RenderBlockContent.small(.init(inlineContent: [.text("hello, world!")])).capitalizingFirstWord()
8686
XCTAssertEqual("Hello, world!", small.rawIndexableTextContent(references: [:]))
8787
}
8888

8989
func testRenderBlockContentHeading() {
90-
let heading = RenderBlockContent.heading(.init(level: 1, text: "hello, world!", anchor: "hi")).withFirstWordCapitalized
90+
let heading = RenderBlockContent.heading(.init(level: 1, text: "hello, world!", anchor: "hi")).capitalizingFirstWord()
9191
XCTAssertEqual("Hello, world!", heading.rawIndexableTextContent(references: [:]))
9292
}
9393

@@ -99,12 +99,12 @@ class RenderBlockContent_CapitalizationTests: XCTestCase {
9999
.init(content: [
100100
.paragraph(.init(inlineContent: [.text("world!")])),
101101
]),
102-
])).withFirstWordCapitalized
102+
])).capitalizingFirstWord()
103103
XCTAssertEqual("hello, world!", list.rawIndexableTextContent(references: [:]))
104104
}
105105

106106
func testRenderBlockContentStep() {
107-
let step = RenderBlockContent.step(.init(content: [.paragraph(.init(inlineContent: [.text("hello, world!")]))], caption: [.paragraph(.init(inlineContent: [.text("Step caption")]))], media: RenderReferenceIdentifier("Media"), code: RenderReferenceIdentifier("Code"), runtimePreview: RenderReferenceIdentifier("Preview"))).withFirstWordCapitalized
107+
let step = RenderBlockContent.step(.init(content: [.paragraph(.init(inlineContent: [.text("hello, world!")]))], caption: [.paragraph(.init(inlineContent: [.text("Step caption")]))], media: RenderReferenceIdentifier("Media"), code: RenderReferenceIdentifier("Code"), runtimePreview: RenderReferenceIdentifier("Preview"))).capitalizingFirstWord()
108108
XCTAssertEqual("hello, world! Step caption", step.rawIndexableTextContent(references: [:]))
109109
}
110110

@@ -117,7 +117,7 @@ class RenderBlockContent_CapitalizationTests: XCTestCase {
117117
.init(content: [
118118
.paragraph(.init(inlineContent: [.text("world!")])),
119119
]),
120-
])).withFirstWordCapitalized
120+
])).capitalizingFirstWord()
121121
XCTAssertEqual("hello, world!", list.rawIndexableTextContent(references: [:]))
122122
}
123123

0 commit comments

Comments
 (0)