Skip to content

Auto-Capitalize First Word #880

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f82b1bb
WIP: first word in parameters and returns is capitalized, all asides …
emilyychenn Mar 27, 2024
c7ecfa6
more WIP not fully functional
emilyychenn Mar 28, 2024
5a63151
WIP
emilyychenn Apr 3, 2024
b7f2429
working version of auto-capitalization
emilyychenn Apr 4, 2024
44538f6
clean up PR
emilyychenn Apr 4, 2024
9f2c6e9
add unit tests
emilyychenn Apr 4, 2024
6a9b916
update documentation year
emilyychenn Apr 4, 2024
97c6557
add unit tests for different alphabets
emilyychenn Apr 5, 2024
4cd7293
change computed property to func to indicate to the caller that this …
emilyychenn Apr 5, 2024
a7e788c
added headings to autocapitalized cases
emilyychenn Apr 5, 2024
11ee2c5
wip resolving some PR comments
emilyychenn Apr 8, 2024
271874c
added end-to-end unit tests
emilyychenn Apr 8, 2024
7dd5823
rename end-to-end test class
emilyychenn Apr 8, 2024
ba4b59b
wip
emilyychenn Apr 9, 2024
c7a9aa3
cleaned up capitalizeFirstWord()
emilyychenn Apr 9, 2024
9dd7ecd
remove unused file
emilyychenn Apr 9, 2024
8e1e8e4
Merge branch 'main' into capitalize-first-word
emilyychenn Apr 9, 2024
da1e76f
fix end-to-end integration tests
emilyychenn Apr 10, 2024
0bfa1a2
remove redundant test
emilyychenn Apr 10, 2024
e3bcdc7
fix failing unit tests
emilyychenn Apr 11, 2024
688f4fa
Merge branch 'main' into capitalize-first-word
emilyychenn Apr 11, 2024
634005d
Update Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/Para…
emilyychenn Apr 12, 2024
38d881f
resolve PR comments
emilyychenn Apr 12, 2024
1e059be
resolve PR comments
emilyychenn Apr 12, 2024
1663871
remove capitalization from extract tag
emilyychenn Apr 12, 2024
a197d5f
remove whitespace
emilyychenn Apr 14, 2024
6b3479b
fix tests for capitalization by locale
emilyychenn Apr 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

/// For auto capitalizing the first letter of a sentence following a colon (e.g. asides, sections such as parameters, returns).
protocol AutoCapitalizable {

/// Any type that conforms to the AutoCapitalizable protocol will have the first letter of the first word capitalized (if applicable).
var withFirstWordCapitalized: Self {
get
}

}

extension AutoCapitalizable {
var withFirstWordCapitalized: Self { return self }
}

extension RenderInlineContent: AutoCapitalizable {
/// Capitalize the first word for normal text content, as well as content that has emphasis or strong applied.
var withFirstWordCapitalized: Self {
switch self {
case .text(let text):
return .text(text.capitalizeFirstWord())
case .emphasis(inlineContent: let embeddedContent):
return .emphasis(inlineContent: [embeddedContent[0].withFirstWordCapitalized] + embeddedContent[1...])
case .strong(inlineContent: let embeddedContent):
return .strong(inlineContent: [embeddedContent[0].withFirstWordCapitalized] + embeddedContent[1...])
default:
return self
}
}
}


extension RenderBlockContent: AutoCapitalizable {
/// Capitalize the first word for paragraphs, asides, headings, and small content.
var withFirstWordCapitalized: Self {
switch self {
case .paragraph(let paragraph):
return .paragraph(paragraph.withFirstWordCapitalized)
case .aside(let aside):
return .aside(aside.withFirstWordCapitalized)
case .small(let small):
return .small(small.withFirstWordCapitalized)
case .heading(let heading):
return .heading(.init(level: heading.level, text: heading.text.capitalizeFirstWord(), anchor: heading.anchor))
default:
return self
}
}
}

extension RenderBlockContent.Paragraph: AutoCapitalizable {
var withFirstWordCapitalized: RenderBlockContent.Paragraph {
guard !self.inlineContent.isEmpty else {
return self
}

let inlineContent = [self.inlineContent[0].withFirstWordCapitalized] + self.inlineContent[1...]
return .init(inlineContent: inlineContent)
}
}

extension RenderBlockContent.Aside: AutoCapitalizable {
var withFirstWordCapitalized: RenderBlockContent.Aside {
guard !self.content.isEmpty else {
return self
}

let content = [self.content[0].withFirstWordCapitalized] + self.content[1...]
return .init(style: self.style, content: content)
}
}

extension RenderBlockContent.Small: AutoCapitalizable {
var withFirstWordCapitalized: RenderBlockContent.Small {
guard !self.inlineContent.isEmpty else {
return self
}

let inlineContent = [self.inlineContent[0].withFirstWordCapitalized] + self.inlineContent[1...]
return .init(inlineContent: inlineContent)
}
}

9 changes: 7 additions & 2 deletions Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,13 @@ struct RenderContentCompiler: MarkupVisitor {

mutating func visitBlockQuote(_ blockQuote: BlockQuote) -> [RenderContent] {
let aside = Aside(blockQuote)
return [RenderBlockContent.aside(.init(style: RenderBlockContent.AsideStyle(asideKind: aside.kind),
content: aside.content.reduce(into: [], { result, child in result.append(contentsOf: visit(child))}) as! [RenderBlockContent]))]

let newAside = RenderBlockContent.Aside(
style: RenderBlockContent.AsideStyle(asideKind: aside.kind),
content: aside.content.reduce(into: [], { result, child in result.append(contentsOf: visit(child))}) as! [RenderBlockContent]
)

return [RenderBlockContent.aside(newAside.withFirstWordCapitalized)]
}

mutating func visitCodeBlock(_ codeBlock: CodeBlock) -> [RenderContent] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This source file is part of the Swift.org open source project

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

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

let capitalizedDiscussionContent = [discussionContent[0].withFirstWordCapitalized] + discussionContent[1...]

let title: String?
if let first = discussionContent.first, case RenderBlockContent.heading = first {
// There's already an authored heading. Don't add another heading.
Expand All @@ -42,7 +44,7 @@ struct DiscussionSectionTranslator: RenderSectionTranslator {
}
}

return ContentRenderSection(kind: .content, content: discussionContent, heading: title)
return ContentRenderSection(kind: .content, content: capitalizedDiscussionContent, heading: title)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This source file is part of the Swift.org open source project

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

See https://swift.org/LICENSE.txt for license information
Expand All @@ -28,7 +28,14 @@ struct ParametersSectionTranslator: RenderSectionTranslator {
let parameterContent = renderNodeTranslator.visitMarkupContainer(
MarkupContainer(parameter.contents)
) as! [RenderBlockContent]
return ParameterRenderSection(name: parameter.name, content: parameterContent)

guard !parameterContent.isEmpty else {
return ParameterRenderSection(name: parameter.name, content: parameterContent)
}

let capitalizedParameterContent = [parameterContent[0].withFirstWordCapitalized] + parameterContent[1...]

return ParameterRenderSection(name: parameter.name, content: capitalizedParameterContent)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
This source file is part of the Swift.org open source project

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

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

return ContentRenderSection(kind: .content, content: returnsContent, heading: "Return Value")
let capitalizedReturnsContent = [returnsContent[0].withFirstWordCapitalized] + returnsContent[1...]

return ContentRenderSection(kind: .content, content: capitalizedReturnsContent, heading: "Return Value")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
This source file is part of the Swift.org open source project

Copyright (c) 2024 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception

See https://swift.org/LICENSE.txt for license information
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
*/

import Foundation

extension String {

// Precomputes the CharacterSet to use in capitalizeFirstWord().
private static let charactersPreventingWordCapitalization = CharacterSet.lowercaseLetters.union(.punctuationCharacters).inverted

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

guard firstWord.rangeOfCharacter(from: Self.charactersPreventingWordCapitalization) == nil else {
return self
}
Comment on lines +22 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the mix of Character and CharacterSet API here confusing. It would be better to stick to one.

This could be either something like

guard let firstWordStartIndex = self.firstIndex(where: { !$0.isWhitespace && !$0.isNewline }) else { return self }
let firstWord = self[firstWordStartIndex...].prefix(while: { !$0.isWhitespace && !$0.isNewline })
guard !firstWord.contains(where: { $0.isLowercase || $0.isPunctuation }) else {
    return self
}

or something like

guard let firstWordStartIndex = rangeOfCharacter(from: .whitespacesAndNewlines.inverted),
      let firstWordEndIndex = rangeOfCharacter(from: .whitespacesAndNewlines, range: firstWordStartIndex.lowerBound..<endIndex)
else { 
    return self
}
let firstWord = self[firstWordEndIndex.lowerBound..<firstWordEndIndex.lowerBound]

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


var resultString = String()
resultString.reserveCapacity(self.count)
resultString.append(contentsOf: self[..<firstWordStartIndex])
resultString.append(contentsOf: String(firstWord).localizedCapitalized)
let restStartIndex = self.index(firstWordStartIndex, offsetBy: firstWord.count)
resultString.append(contentsOf: self[restStartIndex...])

return resultString
}
}
Loading