diff --git a/Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent+Capitalization.swift b/Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent+Capitalization.swift new file mode 100644 index 0000000000..06614e5621 --- /dev/null +++ b/Sources/SwiftDocC/Model/Rendering/Content/RenderBlockContent+Capitalization.swift @@ -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) + } +} + diff --git a/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift b/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift index 6476ec7f30..8d70847b26 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderContentCompiler.swift @@ -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] { diff --git a/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DiscussionSectionTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DiscussionSectionTranslator.swift index bac3a2133b..5b6594a5b7 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DiscussionSectionTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/DiscussionSectionTranslator.swift @@ -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 @@ -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. @@ -42,7 +44,7 @@ struct DiscussionSectionTranslator: RenderSectionTranslator { } } - return ContentRenderSection(kind: .content, content: discussionContent, heading: title) + return ContentRenderSection(kind: .content, content: capitalizedDiscussionContent, heading: title) } } } diff --git a/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/ParametersSectionTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/ParametersSectionTranslator.swift index afb99f96c3..66c4238806 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/ParametersSectionTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/ParametersSectionTranslator.swift @@ -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 @@ -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) } ) } diff --git a/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/ReturnsSectionTranslator.swift b/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/ReturnsSectionTranslator.swift index 3d31c01cf1..e10985f0b0 100644 --- a/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/ReturnsSectionTranslator.swift +++ b/Sources/SwiftDocC/Model/Rendering/RenderSectionTranslator/ReturnsSectionTranslator.swift @@ -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 @@ -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") } } } diff --git a/Sources/SwiftDocC/Utility/FoundationExtensions/String+Capitalization.swift b/Sources/SwiftDocC/Utility/FoundationExtensions/String+Capitalization.swift new file mode 100644 index 0000000000..3f27074c22 --- /dev/null +++ b/Sources/SwiftDocC/Utility/FoundationExtensions/String+Capitalization.swift @@ -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 + } + + var resultString = String() + resultString.reserveCapacity(self.count) + resultString.append(contentsOf: self[.. SymbolGraph { + makeSymbolGraph( + docComment: docComment, + sourceLanguage: .swift, + parameters: [ + ("firstParameter", nil), + ("secondParameter", nil), + ("thirdParameter", nil), + ("fourthParameter", nil), + ], + returnValue: .init(kind: .typeIdentifier, spelling: "ReturnValue", preciseIdentifier: "return-value-id") + ) + } + + private func makeSymbolGraph( + docComment: String?, + sourceLanguage: SourceLanguage, + parameters: [(name: String, externalName: String?)], + returnValue: SymbolGraph.Symbol.DeclarationFragments.Fragment + ) -> SymbolGraph { + let uri = symbolURL.absoluteString // we want to include the file:// scheme here + func makeLineList(text: String) -> SymbolGraph.LineList { + return .init(text.splitByNewlines.enumerated().map { lineOffset, line in + .init(text: line, range: .init(start: .init(line: start.line + lineOffset, character: start.character), + end: .init(line: start.line + lineOffset, character: start.character + line.count))) + }, uri: uri) + } + + return makeSymbolGraph( + moduleName: "ModuleName", + symbols: [ + .init( + identifier: .init(precise: "symbol-id", interfaceLanguage: sourceLanguage.id), + names: .init(title: "functionName(...)", navigator: nil, subHeading: nil, prose: nil), + pathComponents: ["functionName(...)"], + docComment: docComment.map { makeLineList(text: $0) }, + accessLevel: .public, kind: .init(parsedIdentifier: .func, displayName: "Function"), + mixins: [ + SymbolGraph.Symbol.Location.mixinKey: SymbolGraph.Symbol.Location(uri: uri, position: start), + + SymbolGraph.Symbol.FunctionSignature.mixinKey: SymbolGraph.Symbol.FunctionSignature( + parameters: parameters.map { + .init(name: $0.name, externalName: $0.externalName, declarationFragments: [], children: []) + }, + returns: [returnValue] + ) + ] + ) + ] + ) + } + + + // MARK: End-to-end integration tests + + func testParametersCapitalization() throws { + let symbolGraph = makeSymbolGraph(docComment: """ + Some symbol description. + + - Parameters: + - one: upper-cased first parameter description. + - two: the second parameter has extra white spaces + - three: inValid third parameter will not be capitalized + - four: `code block` will not be capitalized + - five: a`nother invalid capitalization + """) + + let url = try createTempFolder(content: [ + Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: symbolGraph) + ]) + ]) + let (_, bundle, context) = try loadBundle(from: url) + + 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 symbol = try XCTUnwrap(node.semantic as? Symbol) + let parameterSections = symbol.parametersSectionVariants + XCTAssertEqual(parameterSections[.swift]?.parameters.map(\.name), ["one", "two", "three", "four", "five"]) + + let parameterSectionTranslator = ParametersSectionTranslator() + var renderNodeTranslator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference, source: url) + var renderNode = renderNodeTranslator.visit(symbol) as! RenderNode + let translatedParameters = parameterSectionTranslator.translateSection(for: symbol, renderNode: &renderNode, renderNodeTranslator: &renderNodeTranslator) + let paramsRenderSection = translatedParameters?.defaultValue?.section as! ParametersRenderSection + + // Different locales treat capitalization of hyphenated words differently (e.g. Upper-Cased vs Upper-cased). + let hyphenatedString = "upper-cased" + let hyphenatedCapitalizedResult = hyphenatedString.localizedCapitalized + " first parameter description." + + XCTAssertEqual(paramsRenderSection.parameters.map(\.content), [ + [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text(hyphenatedCapitalizedResult)]))], + [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text("The second parameter has extra white spaces")]))], + [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text("inValid third parameter will not be capitalized")]))], + [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text(""), SwiftDocC.RenderInlineContent.codeVoice(code: "code block"), SwiftDocC.RenderInlineContent.text(" will not be capitalized")]))], + [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text("a`nother invalid capitalization")]))]]) + } + + func testIndividualParametersCapitalization() throws { + let symbolGraph = makeSymbolGraph(docComment: """ + Some symbol description. + + - parameter one: upper-cased first parameter description. + - parameter two: the second parameter has extra white spaces + - parameter three: inValid third parameter will not be capitalized + - parameter four: `code block` will not be capitalized + - parameter five: a`nother invalid capitalization + """) + + let url = try createTempFolder(content: [ + Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: symbolGraph) + ]) + ]) + let (_, bundle, context) = try loadBundle(from: url) + + XCTAssertEqual(context.problems.count, 0) + + let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/ModuleName/functionName(...)", sourceLanguage: .swift) + 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"]) + + let parameterSectionTranslator = ParametersSectionTranslator() + var renderNodeTranslator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference, source: url) + var renderNode = renderNodeTranslator.visit(symbol) as! RenderNode + let translatedParameters = parameterSectionTranslator.translateSection(for: symbol, renderNode: &renderNode, renderNodeTranslator: &renderNodeTranslator) + let paramsRenderSection = translatedParameters?.defaultValue?.section as! ParametersRenderSection + + // Different locales treat capitalization of hyphenated words differently (e.g. Upper-Cased vs Upper-cased). + let hyphenatedString = "upper-cased" + let hyphenatedCapitalizedResult = hyphenatedString.localizedCapitalized + " first parameter description." + + XCTAssertEqual(paramsRenderSection.parameters.map(\.content), [ + [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text(hyphenatedCapitalizedResult)]))], + [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text("The second parameter has extra white spaces")]))], + [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text("inValid third parameter will not be capitalized")]))], + [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text(""), SwiftDocC.RenderInlineContent.codeVoice(code: "code block"), SwiftDocC.RenderInlineContent.text(" will not be capitalized")]))], + [SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text("a`nother invalid capitalization")]))]]) + } + + func testReturnsCapitalization() throws { + let symbolGraph = makeSymbolGraph(docComment: """ + Some symbol description. + + - Returns: string, first word should have been capitalized here. + """) + + let url = try createTempFolder(content: [ + Folder(name: "unit-test.docc", content: [ + JSONFile(name: "ModuleName.symbols.json", content: symbolGraph) + ]) + ]) + let (_, bundle, context) = try loadBundle(from: url) + + XCTAssertEqual(context.problems.count, 0) + + let reference = ResolvedTopicReference(bundleIdentifier: bundle.identifier, path: "/documentation/ModuleName/functionName(...)", sourceLanguage: .swift) + let node = try context.entity(with: reference) + let symbol = try XCTUnwrap(node.semantic as? Symbol) + + let returnsSectionTranslator = ReturnsSectionTranslator() + var renderNodeTranslator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference, source: url) + var renderNode = renderNodeTranslator.visit(symbol) as! RenderNode + let translatedReturns = returnsSectionTranslator.translateSection(for: symbol, renderNode: &renderNode, renderNodeTranslator: &renderNodeTranslator) + let returnsRenderSection = translatedReturns?.defaultValue?.section as! ContentRenderSection + + XCTAssertEqual(returnsRenderSection.content, [SwiftDocC.RenderBlockContent.heading(SwiftDocC.RenderBlockContent.Heading(level: 2, text: "Return Value", anchor: Optional("return-value"))), SwiftDocC.RenderBlockContent.paragraph(SwiftDocC.RenderBlockContent.Paragraph(inlineContent: [SwiftDocC.RenderInlineContent.text("String, first word should have been capitalized here.")]))]) + } +} diff --git a/Tests/SwiftDocCTests/Model/RenderBlockContent+CapitalizationTests.swift b/Tests/SwiftDocCTests/Model/RenderBlockContent+CapitalizationTests.swift new file mode 100644 index 0000000000..cb1e312f97 --- /dev/null +++ b/Tests/SwiftDocCTests/Model/RenderBlockContent+CapitalizationTests.swift @@ -0,0 +1,124 @@ +/* + 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 +import XCTest +import Markdown +@testable import SwiftDocC + +class RenderBlockContent_CapitalizationTests: XCTestCase { + + // MARK: - Inlines + // Text, Emphasis, Strong are all auto-capitalized, and everything else defaults to not capitalized. + + func testRenderInlineContentText() { + let text = RenderInlineContent.text("hello, world!").withFirstWordCapitalized + XCTAssertEqual("Hello, world!", text.plainText) + } + + func testRenderInlineContentEmphasis() { + let emphasis = RenderInlineContent.emphasis(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized + XCTAssertEqual("Hello, world!", emphasis.plainText) + } + + func testRenderInlineContentStrong() { + let strong = RenderInlineContent.strong(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized + XCTAssertEqual("Hello, world!", strong.plainText) + } + + func testRenderInlineContentCodeVoice() { + let codeVoice = RenderInlineContent.codeVoice(code: "code voice").withFirstWordCapitalized + XCTAssertEqual("code voice", codeVoice.plainText) + } + + func testRenderInlineContentReference() { + let reference = RenderInlineContent.reference(identifier: .init("Test"), isActive: true, overridingTitle: "hello, world!", overridingTitleInlineContent: [.text("hello, world!")]).withFirstWordCapitalized + XCTAssertEqual("hello, world!", reference.plainText) + } + + func testRenderInlineContentNewTerm() { + let newTerm = RenderInlineContent.newTerm(inlineContent: [.text("helloWorld")]).withFirstWordCapitalized + XCTAssertEqual("helloWorld", newTerm.plainText) + } + + func testRenderInlineContentInlineHead() { + let inlineHead = RenderInlineContent.inlineHead(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized + XCTAssertEqual("hello, world!", inlineHead.plainText) + } + + func testRenderInlineContentSubscript() { + let subscriptContent = RenderInlineContent.subscript(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized + XCTAssertEqual("hello, world!", subscriptContent.plainText) + } + + func testRenderInlineContentSuperscript() { + let superscriptContent = RenderInlineContent.superscript(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized + XCTAssertEqual("hello, world!", superscriptContent.plainText) + } + + func testRenderInlineContentStrikethrough() { + let strikethrough = RenderInlineContent.strikethrough(inlineContent: [.text("hello, world!")]).withFirstWordCapitalized + 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 + 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 + XCTAssertEqual("Hello, world!", aside.rawIndexableTextContent(references: [:])) + } + + func testRenderBlockContentSmall() { + let small = RenderBlockContent.small(.init(inlineContent: [.text("hello, world!")])).withFirstWordCapitalized + XCTAssertEqual("Hello, world!", small.rawIndexableTextContent(references: [:])) + } + + func testRenderBlockContentHeading() { + let heading = RenderBlockContent.heading(.init(level: 1, text: "hello, world!", anchor: "hi")).withFirstWordCapitalized + XCTAssertEqual("Hello, world!", heading.rawIndexableTextContent(references: [:])) + } + + func testRenderBlockContentUnorderedList() { + let list = RenderBlockContent.unorderedList(.init(items: [ + .init(content: [ + .paragraph(.init(inlineContent: [.text("hello,")])), + ]), + .init(content: [ + .paragraph(.init(inlineContent: [.text("world!")])), + ]), + ])).withFirstWordCapitalized + 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 + XCTAssertEqual("hello, world! Step caption", step.rawIndexableTextContent(references: [:])) + } + + + func testRenderBlockContentOrderedList() { + let list = RenderBlockContent.orderedList(.init(items: [ + .init(content: [ + .paragraph(.init(inlineContent: [.text("hello,")])), + ]), + .init(content: [ + .paragraph(.init(inlineContent: [.text("world!")])), + ]), + ])).withFirstWordCapitalized + XCTAssertEqual("hello, world!", list.rawIndexableTextContent(references: [:])) + } + +} diff --git a/Tests/SwiftDocCTests/Utility/String+CapitalizationTests.swift b/Tests/SwiftDocCTests/Utility/String+CapitalizationTests.swift new file mode 100644 index 0000000000..0c9c6f15f7 --- /dev/null +++ b/Tests/SwiftDocCTests/Utility/String+CapitalizationTests.swift @@ -0,0 +1,71 @@ +/* + 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 XCTest +@testable import SwiftDocC + +class String_CapitalizationTests: XCTestCase { + + func testAllLowerCase() { + let testString = "hello world" + XCTAssertEqual("Hello world", testString.capitalizeFirstWord()) + } + + func testAllLowerCaseWithPunctuation() { + let testString1 = "hello, world" + let testString2 = "twenty-one" + let testString3 = "hello! world" + let testString4 = "hello: world" + let testString5 = "l'ocean world" + XCTAssertEqual("Hello, world", testString1.capitalizeFirstWord()) + XCTAssertEqual("Twenty-One", testString2.capitalizeFirstWord()) + XCTAssertEqual("Hello! world", testString3.capitalizeFirstWord()) + XCTAssertEqual("Hello: world", testString4.capitalizeFirstWord()) + XCTAssertEqual("L'ocean world", testString5.capitalizeFirstWord()) + } + + func testInvalidPunctuation() { + let testString = "h`ello world" + XCTAssertEqual(testString, testString.capitalizeFirstWord()) + } + + func testHasUppercase() { + let testString = "iPad iOS visionOS" + XCTAssertEqual(testString, testString.capitalizeFirstWord()) + } + + func testWhiteSpaces() { + let testString1 = " has many spaces" + let testString2 = " has a tab" + let testString3 = " has many spaces " + XCTAssertEqual(" Has many spaces", testString1.capitalizeFirstWord()) + XCTAssertEqual(" Has a tab", testString2.capitalizeFirstWord()) + XCTAssertEqual(" Has many spaces ", testString3.capitalizeFirstWord()) + } + + + func testDifferentAlphabets() { + let testString1 = "l'amérique du nord" + let testString2 = "ça va?" + let testString3 = "à" + let testString4 = "チーズ" + let testString5 = "牛奶" + let testString6 = "i don't like 牛奶" + let testString7 = "牛奶 is tasty" + XCTAssertEqual("L'amérique du nord", testString1.capitalizeFirstWord()) + XCTAssertEqual("Ça va?", testString2.capitalizeFirstWord()) + XCTAssertEqual("À", testString3.capitalizeFirstWord()) + XCTAssertEqual("チーズ", testString4.capitalizeFirstWord()) + XCTAssertEqual("牛奶", testString5.capitalizeFirstWord()) + XCTAssertEqual("I don't like 牛奶", testString6.capitalizeFirstWord()) + XCTAssertEqual("牛奶 is tasty", testString7.capitalizeFirstWord()) + } + +}