Skip to content

Fix to auto-capitalization PR #888

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 3 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -8,85 +8,73 @@
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 {
extension RenderInlineContent {
/// Capitalize the first word for normal text content, as well as content that has emphasis or strong applied.
var withFirstWordCapitalized: Self {
func capitalizingFirstWord() -> Self {
switch self {
case .text(let text):
return .text(text.capitalizeFirstWord())
return .text(text.capitalizingFirstWord())
case .emphasis(inlineContent: let embeddedContent):
return .emphasis(inlineContent: [embeddedContent[0].withFirstWordCapitalized] + embeddedContent[1...])
return .emphasis(inlineContent: embeddedContent.capitalizingFirstWord())
case .strong(inlineContent: let embeddedContent):
return .strong(inlineContent: [embeddedContent[0].withFirstWordCapitalized] + embeddedContent[1...])
return .strong(inlineContent: embeddedContent.capitalizingFirstWord())
default:
return self
}
}
}

extension [RenderBlockContent] {
func capitalizingFirstWord() -> Self {
guard let first else { return [] }

return [first.capitalizingFirstWord()] + dropFirst()
}
}

extension [RenderInlineContent] {
func capitalizingFirstWord() -> Self {
guard let first else { return [] }

return [first.capitalizingFirstWord()] + dropFirst()
}
}

extension RenderBlockContent: AutoCapitalizable {

extension RenderBlockContent {
/// Capitalize the first word for paragraphs, asides, headings, and small content.
var withFirstWordCapitalized: Self {
func capitalizingFirstWord() -> Self {
switch self {
case .paragraph(let paragraph):
return .paragraph(paragraph.withFirstWordCapitalized)
return .paragraph(paragraph.capitalizingFirstWord())
case .aside(let aside):
return .aside(aside.withFirstWordCapitalized)
return .aside(aside.capitalizingFirstWord())
case .small(let small):
return .small(small.withFirstWordCapitalized)
return .small(small.capitalizingFirstWord())
case .heading(let heading):
return .heading(.init(level: heading.level, text: heading.text.capitalizeFirstWord(), anchor: heading.anchor))
return .heading(.init(level: heading.level, text: heading.text.capitalizingFirstWord(), 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.Paragraph {
func capitalizingFirstWord() -> RenderBlockContent.Paragraph {
return .init(inlineContent: inlineContent.capitalizingFirstWord())
}
}

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.Aside {
func capitalizingFirstWord() -> RenderBlockContent.Aside {
return .init(style: self.style, content: self.content.capitalizingFirstWord())
}
}

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)
extension RenderBlockContent.Small {
func capitalizingFirstWord() -> RenderBlockContent.Small {
return .init(inlineContent: self.inlineContent.capitalizingFirstWord())
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ struct RenderContentCompiler: MarkupVisitor {
content: aside.content.reduce(into: [], { result, child in result.append(contentsOf: visit(child))}) as! [RenderBlockContent]
)

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

mutating func visitCodeBlock(_ codeBlock: CodeBlock) -> [RenderContent] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ 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 @@ -44,7 +42,7 @@ struct DiscussionSectionTranslator: RenderSectionTranslator {
}
}

return ContentRenderSection(kind: .content, content: capitalizedDiscussionContent, heading: title)
return ContentRenderSection(kind: .content, content: discussionContent.capitalizingFirstWord(), heading: title)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ struct ParametersSectionTranslator: RenderSectionTranslator {
return ParameterRenderSection(name: parameter.name, content: parameterContent)
}

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

return ParameterRenderSection(name: parameter.name, content: capitalizedParameterContent)
return ParameterRenderSection(name: parameter.name, content: parameterContent.capitalizingFirstWord())
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ struct ReturnsSectionTranslator: RenderSectionTranslator {
return nil
}

let capitalizedReturnsContent = [returnsContent[0].withFirstWordCapitalized] + returnsContent[1...]

return ContentRenderSection(kind: .content, content: capitalizedReturnsContent, heading: "Return Value")
return ContentRenderSection(kind: .content, content: returnsContent.capitalizingFirstWord(), heading: "Return Value")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,23 @@ import Foundation

extension String {

// Precomputes the CharacterSet to use in capitalizeFirstWord().
// Precomputes the CharacterSet to use in capitalizingFirstWord().
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 {
func capitalizingFirstWord() -> 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
}

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...])
var resultString = self

resultString.replaceSubrange(firstWordStartIndex..<firstWord.endIndex, with: firstWord.localizedCapitalized)

return resultString
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class AutoCapitalizationTests: XCTestCase {
XCTAssertEqual(context.problems.count, 0)

let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/ModuleName/functionName(...)", sourceLanguage: .swift)
var node = try context.entity(with: reference)
let node = try context.entity(with: reference)
let symbol = try XCTUnwrap(node.semantic as? Symbol)
let parameterSections = symbol.parametersSectionVariants
XCTAssertEqual(parameterSections[.swift]?.parameters.map(\.name), ["one", "two", "three", "four", "five"])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,75 +19,75 @@ class RenderBlockContent_CapitalizationTests: XCTestCase {
// Text, Emphasis, Strong are all auto-capitalized, and everything else defaults to not capitalized.

func testRenderInlineContentText() {
let text = RenderInlineContent.text("hello, world!").withFirstWordCapitalized
let text = RenderInlineContent.text("hello, world!").capitalizingFirstWord()
XCTAssertEqual("Hello, world!", text.plainText)
}

func testRenderInlineContentEmphasis() {
let emphasis = RenderInlineContent.emphasis(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
let emphasis = RenderInlineContent.emphasis(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
XCTAssertEqual("Hello, world!", emphasis.plainText)
}

func testRenderInlineContentStrong() {
let strong = RenderInlineContent.strong(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
let strong = RenderInlineContent.strong(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
XCTAssertEqual("Hello, world!", strong.plainText)
}

func testRenderInlineContentCodeVoice() {
let codeVoice = RenderInlineContent.codeVoice(code: "code voice").withFirstWordCapitalized
let codeVoice = RenderInlineContent.codeVoice(code: "code voice").capitalizingFirstWord()
XCTAssertEqual("code voice", codeVoice.plainText)
}

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

func testRenderInlineContentNewTerm() {
let newTerm = RenderInlineContent.newTerm(inlineContent: [.text("helloWorld")]).withFirstWordCapitalized
let newTerm = RenderInlineContent.newTerm(inlineContent: [.text("helloWorld")]).capitalizingFirstWord()
XCTAssertEqual("helloWorld", newTerm.plainText)
}

func testRenderInlineContentInlineHead() {
let inlineHead = RenderInlineContent.inlineHead(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
let inlineHead = RenderInlineContent.inlineHead(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
XCTAssertEqual("hello, world!", inlineHead.plainText)
}

func testRenderInlineContentSubscript() {
let subscriptContent = RenderInlineContent.subscript(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
let subscriptContent = RenderInlineContent.subscript(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
XCTAssertEqual("hello, world!", subscriptContent.plainText)
}

func testRenderInlineContentSuperscript() {
let superscriptContent = RenderInlineContent.superscript(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
let superscriptContent = RenderInlineContent.superscript(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
XCTAssertEqual("hello, world!", superscriptContent.plainText)
}

func testRenderInlineContentStrikethrough() {
let strikethrough = RenderInlineContent.strikethrough(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized
let strikethrough = RenderInlineContent.strikethrough(inlineContent: [.text("hello, world!")]).capitalizingFirstWord()
XCTAssertEqual("hello, world!", strikethrough.plainText)
}

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

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

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

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

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

Expand All @@ -99,12 +99,12 @@ class RenderBlockContent_CapitalizationTests: XCTestCase {
.init(content: [
.paragraph(.init(inlineContent: [.text("world!")])),
]),
])).withFirstWordCapitalized
])).capitalizingFirstWord()
XCTAssertEqual("hello, world!", list.rawIndexableTextContent(references: [:]))
}

func testRenderBlockContentStep() {
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
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()
XCTAssertEqual("hello, world! Step caption", step.rawIndexableTextContent(references: [:]))
}

Expand All @@ -117,7 +117,7 @@ class RenderBlockContent_CapitalizationTests: XCTestCase {
.init(content: [
.paragraph(.init(inlineContent: [.text("world!")])),
]),
])).withFirstWordCapitalized
])).capitalizingFirstWord()
XCTAssertEqual("hello, world!", list.rawIndexableTextContent(references: [:]))
}

Expand Down
Loading