Skip to content

Commit 30110b0

Browse files
Auto-Capitalize First Word (#880)
rdar://122167705 When writing a doc comment, it feels intuitive to not capitalise the first word of its description because of the nature of the doc comment. For example: adding a new parameter would be written as: `/// - Parameter testParam: this parameter is just a test parameter to show what this looks like in lowercase.` In this example, the sentence in doc comments is natural to write with a lowercase starting word, because the first word (Parameter) is already capitalized. This PR auto-capitalizes the first word of a new section or aside. Note that this auto-capitalization only occurs if the first word is all lowercase and contains only characters A-Z, or if the first word contains CharacterSet punctuation characters (e.g. a period, comma, hyphen, semi-colon, colon). This capitalization is also locale specific. --------- Co-authored-by: Pat Shaughnessy <[email protected]>
1 parent 29332fe commit 30110b0

File tree

9 files changed

+547
-8
lines changed

9 files changed

+547
-8
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 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+
/// 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+
}
24+
25+
extension RenderInlineContent: AutoCapitalizable {
26+
/// Capitalize the first word for normal text content, as well as content that has emphasis or strong applied.
27+
var withFirstWordCapitalized: Self {
28+
switch self {
29+
case .text(let text):
30+
return .text(text.capitalizeFirstWord())
31+
case .emphasis(inlineContent: let embeddedContent):
32+
return .emphasis(inlineContent: [embeddedContent[0].withFirstWordCapitalized] + embeddedContent[1...])
33+
case .strong(inlineContent: let embeddedContent):
34+
return .strong(inlineContent: [embeddedContent[0].withFirstWordCapitalized] + embeddedContent[1...])
35+
default:
36+
return self
37+
}
38+
}
39+
}
40+
41+
42+
extension RenderBlockContent: AutoCapitalizable {
43+
/// Capitalize the first word for paragraphs, asides, headings, and small content.
44+
var withFirstWordCapitalized: Self {
45+
switch self {
46+
case .paragraph(let paragraph):
47+
return .paragraph(paragraph.withFirstWordCapitalized)
48+
case .aside(let aside):
49+
return .aside(aside.withFirstWordCapitalized)
50+
case .small(let small):
51+
return .small(small.withFirstWordCapitalized)
52+
case .heading(let heading):
53+
return .heading(.init(level: heading.level, text: heading.text.capitalizeFirstWord(), anchor: heading.anchor))
54+
default:
55+
return self
56+
}
57+
}
58+
}
59+
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)
68+
}
69+
}
70+
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)
79+
}
80+
}
81+
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)
90+
}
91+
}
92+

Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,13 @@ struct RenderContentCompiler: MarkupVisitor {
3636

3737
mutating func visitBlockQuote(_ blockQuote: BlockQuote) -> [RenderContent] {
3838
let aside = Aside(blockQuote)
39-
return [RenderBlockContent.aside(.init(style: RenderBlockContent.AsideStyle(asideKind: aside.kind),
40-
content: aside.content.reduce(into: [], { result, child in result.append(contentsOf: visit(child))}) as! [RenderBlockContent]))]
39+
40+
let newAside = RenderBlockContent.Aside(
41+
style: RenderBlockContent.AsideStyle(asideKind: aside.kind),
42+
content: aside.content.reduce(into: [], { result, child in result.append(contentsOf: visit(child))}) as! [RenderBlockContent]
43+
)
44+
45+
return [RenderBlockContent.aside(newAside.withFirstWordCapitalized)]
4146
}
4247

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

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -27,6 +27,8 @@ struct DiscussionSectionTranslator: RenderSectionTranslator {
2727
return nil
2828
}
2929

30+
let capitalizedDiscussionContent = [discussionContent[0].withFirstWordCapitalized] + discussionContent[1...]
31+
3032
let title: String?
3133
if let first = discussionContent.first, case RenderBlockContent.heading = first {
3234
// There's already an authored heading. Don't add another heading.
@@ -42,7 +44,7 @@ struct DiscussionSectionTranslator: RenderSectionTranslator {
4244
}
4345
}
4446

45-
return ContentRenderSection(kind: .content, content: discussionContent, heading: title)
47+
return ContentRenderSection(kind: .content, content: capitalizedDiscussionContent, heading: title)
4648
}
4749
}
4850
}

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -28,7 +28,14 @@ struct ParametersSectionTranslator: RenderSectionTranslator {
2828
let parameterContent = renderNodeTranslator.visitMarkupContainer(
2929
MarkupContainer(parameter.contents)
3030
) as! [RenderBlockContent]
31-
return ParameterRenderSection(name: parameter.name, content: parameterContent)
31+
32+
guard !parameterContent.isEmpty else {
33+
return ParameterRenderSection(name: parameter.name, content: parameterContent)
34+
}
35+
36+
let capitalizedParameterContent = [parameterContent[0].withFirstWordCapitalized] + parameterContent[1...]
37+
38+
return ParameterRenderSection(name: parameter.name, content: capitalizedParameterContent)
3239
}
3340
)
3441
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/*
22
This source file is part of the Swift.org open source project
33

4-
Copyright (c) 2021 Apple Inc. and the Swift project authors
4+
Copyright (c) 2021-2024 Apple Inc. and the Swift project authors
55
Licensed under Apache License v2.0 with Runtime Library Exception
66

77
See https://swift.org/LICENSE.txt for license information
@@ -28,7 +28,9 @@ struct ReturnsSectionTranslator: RenderSectionTranslator {
2828
return nil
2929
}
3030

31-
return ContentRenderSection(kind: .content, content: returnsContent, heading: "Return Value")
31+
let capitalizedReturnsContent = [returnsContent[0].withFirstWordCapitalized] + returnsContent[1...]
32+
33+
return ContentRenderSection(kind: .content, content: capitalizedReturnsContent, heading: "Return Value")
3234
}
3335
}
3436
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2024 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 String {
14+
15+
// Precomputes the CharacterSet to use in capitalizeFirstWord().
16+
private static let charactersPreventingWordCapitalization = CharacterSet.lowercaseLetters.union(.punctuationCharacters).inverted
17+
18+
/// Returns the string with the first letter capitalized.
19+
/// This auto-capitalization only occurs if the first word is all lowercase and contains only lowercase letters.
20+
/// The first word can also contain punctuation (e.g. a period, comma, hyphen, semi-colon, colon).
21+
func capitalizeFirstWord() -> String {
22+
guard let firstWordStartIndex = self.firstIndex(where: { !$0.isWhitespace && !$0.isNewline }) else { return self }
23+
let firstWord = self[firstWordStartIndex...].prefix(while: { !$0.isWhitespace && !$0.isNewline})
24+
25+
guard firstWord.rangeOfCharacter(from: Self.charactersPreventingWordCapitalization) == nil else {
26+
return self
27+
}
28+
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...])
35+
36+
return resultString
37+
}
38+
}

0 commit comments

Comments
 (0)