Skip to content
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
31 changes: 4 additions & 27 deletions Sources/CLI/renderer/DefaultDocumentationTypeListRenderer.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Foundation

struct DefaultDocumentationTypeListRenderer: Sendable {
enum Output: Sendable {
case table
Expand All @@ -17,35 +15,14 @@ struct DefaultDocumentationTypeListRenderer: Sendable {
}

func render(_ types: [DocumentationType]) throws -> String {
let presentation = DocumentationPresenter().symbols(types, technology: technology, audience: audience)
switch output {
case .table:
if audience == .agent {
let presentation = DocumentationPresenter().symbols(types, technology: technology, audience: .agent)
return AgentDocumentationRenderer().render(presentation, technology: technology)
}
return terminalSafeText(renderTable(types))
return audience == .agent
? AgentDocumentationRenderer().render(presentation, technology: technology)
: HumanDocumentationRenderer().render(presentation)
case .json:
let presentation = DocumentationPresenter().symbols(types, technology: technology, audience: audience)
return try StructuredDocumentationRenderer().render(presentation)
}
}

private func renderTable(_ types: [DocumentationType]) -> String {
guard !types.isEmpty else { return "No symbols found." }
let nameWidth = max("SYMBOL".count, types.map(\.name.count).max() ?? 0)
let kindWidth = max("KIND".count, types.map(\.kind.count).max() ?? 0)
let pathWidth = max("PATH".count, types.map(\.path.count).max() ?? 0)

let heading =
"SYMBOL".padding(toLength: nameWidth, withPad: " ", startingAt: 0) + " "
+ "KIND".padding(toLength: kindWidth, withPad: " ", startingAt: 0) + " "
+ "PATH".padding(toLength: pathWidth, withPad: " ", startingAt: 0) + " URL"
let rows = types.map { type in
type.name.padding(toLength: nameWidth, withPad: " ", startingAt: 0) + " "
+ type.kind.padding(toLength: kindWidth, withPad: " ", startingAt: 0) + " "
+ type.path.padding(toLength: pathWidth, withPad: " ", startingAt: 0) + " "
+ type.url
}
return ([heading] + rows).joined(separator: "\n")
}
}
22 changes: 4 additions & 18 deletions Sources/CLI/renderer/DefaultTechnologyListRenderer.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Foundation

struct DefaultTechnologyListRenderer: Sendable {
enum Output: Sendable {
case table
Expand All @@ -15,26 +13,14 @@ struct DefaultTechnologyListRenderer: Sendable {
}

func render(_ technologies: [Technology]) throws -> String {
let presentation = DocumentationPresenter().technologies(technologies, audience: audience)
switch output {
case .table:
if audience == .agent {
let presentation = DocumentationPresenter().technologies(technologies, audience: .agent)
return AgentDocumentationRenderer().render(presentation)
}
return terminalSafeText(renderTable(technologies))
return audience == .agent
? AgentDocumentationRenderer().render(presentation)
: HumanDocumentationRenderer().render(presentation)
case .json:
let presentation = DocumentationPresenter().technologies(technologies, audience: audience)
return try StructuredDocumentationRenderer().render(presentation)
}
}

private func renderTable(_ technologies: [Technology]) -> String {
let heading = "TECHNOLOGY"
let width = max(heading.count, technologies.map(\.name.count).max() ?? 0)
let rows = technologies.map {
$0.name + String(repeating: " ", count: width - $0.name.count) + " " + $0.identifier
}
return ([heading + String(repeating: " ", count: width - heading.count) + " IDENTIFIER"] + rows)
.joined(separator: "\n")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct DefaultTypeDocumentationRenderer: Sendable {
case .text:
return audience == .agent
? AgentDocumentationRenderer().render(presentation)
: TextTypeDocumentationRenderer().render(page)
: HumanDocumentationRenderer().render(presentation)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Foundation

struct TextTypeDocumentationRenderer: Sendable {
struct HumanDocumentationRenderer: Sendable {
private let layout = DocumentationTextLayout()
private let content = DocumentationContentRenderer(audience: .human)

func render(_ page: DocumentationPage) -> String {
func render(_ presentation: PagePresentation) -> String {
let page = presentation.document
let metadata = ([page.roleHeading ?? page.kind.capitalized] + page.modules)
.filter { !$0.isEmpty }.joined(separator: " · ")
var header = layout.heading(page.title, prominent: true) + "\n" + metadata
Expand All @@ -24,25 +25,47 @@ struct TextTypeDocumentationRenderer: Sendable {
}.joined(separator: "\n\n"), to: &sections)
let body = content.blocks(page.content, indent: " ")
if !body.isEmpty { sections.append(body) }
sections += groups(page.relationships)
appendGroups("Topics", page.topics, to: &sections)
appendGroups("See Also", page.seeAlso, to: &sections)
sections += groups(presentation.relationships)
appendGroups("Topics", presentation.topics, to: &sections)
appendGroups("See Also", presentation.seeAlso, to: &sections)
sections.append(layout.heading("Documentation") + "\n " + page.url.absoluteString)
return terminalSafeText(sections.joined(separator: "\n\n"))
}

func render(_ symbols: [SymbolPresentation]) -> String {
guard !symbols.isEmpty else { return "No symbols found." }
return table(
headers: ["SYMBOL", "KIND", "PATH", "URL"],
rows: symbols.map { [$0.name, $0.kind, $0.path, $0.url] })
}

func render(_ technologies: [TechnologyPresentation]) -> String {
table(headers: ["TECHNOLOGY", "IDENTIFIER"], rows: technologies.map { [$0.name, $0.identifier] })
}

private func table(headers: [String], rows: [[String]]) -> String {
let values = ([headers] + rows).map { $0.map(terminalSafeText) }
let widths = headers.indices.map { column in values.map { $0[column].count }.max() ?? 0 }
return values.map { row in
row.enumerated().map { column, value in
column == row.count - 1 ? value : value + String(repeating: " ", count: widths[column] - value.count)
}.joined(separator: " ")
}.joined(separator: "\n")
}

private func append(_ title: String, _ body: String, to sections: inout [String]) {
if !body.isEmpty { sections.append(layout.heading(title) + "\n" + body) }
}

private func appendGroups(_ title: String, _ entries: [DocumentationGroup], to sections: inout [String]) {
private func appendGroups(_ title: String, _ entries: [GroupPresentation], to sections: inout [String]) {
let rendered = groups(entries)
if !rendered.isEmpty { sections += [layout.heading(title, prominent: true)] + rendered }
}

private func groups(_ groups: [DocumentationGroup]) -> [String] {
private func groups(_ groups: [GroupPresentation]) -> [String] {
groups.compactMap { group in
let entries = group.references.map { reference in
let entries = group.references.map { item in
let reference = item.reference
var text = layout.paragraph(reference.title, indent: " ", firstPrefix: " • ")
let abstract = content.inline(reference.abstract)
if !abstract.isEmpty { text += "\n" + layout.paragraph(abstract, indent: " ") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,18 @@ struct AppleDocsCommandIntegrationTests {
func rendersAgentJSON() throws {
// -- Arrange --
let arguments = ["types", "view", "String", "--technology", "Swift", "--agent", "--json", "--verbose"]
var diagnostics = ""

// -- Act --
let output = try runAppleDocs(arguments)
let output = try runAppleDocs(arguments, captureStandardError: { diagnostics = $0 })
let value = try #require(JSONSerialization.jsonObject(with: Data(output.utf8)) as? [String: Any])

// -- Assert --
#expect(value["title"] as? String == "String")
#expect(value["navigation"] != nil)
#expect(value["metadata"] == nil)
#expect(!output.contains("\u{1B}"))
#expect(diagnostics.contains("debug"))
}

@Test("returns Foundation URL documentation as JSON")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,29 @@ struct DefaultDocumentationTypeListRendererTests {
)
}

@Test("aligns table columns after removing remote control characters")
func alignsSanitizedTable() throws {
// -- Arrange --
let types = [
DocumentationType(
name: "Model()\u{0007}", kind: "macro\u{0007}", path: "model()\u{0007}",
url: "https://developer.apple.com/documentation/swiftdata/model()\u{0007}"
)
]
let renderer = DefaultDocumentationTypeListRenderer(output: .table)

// -- Act --
let output = try renderer.render(types)

// -- Assert --
#expect(
output == """
SYMBOL KIND PATH URL
Model() macro model() https://developer.apple.com/documentation/swiftdata/model()
"""
)
}

@Test("renders documentation types as JSON")
func rendersJSON() throws {
// -- Arrange --
Expand Down
35 changes: 35 additions & 0 deletions Tests/CLITests/renderer/DefaultTechnologyListRendererTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,41 @@ struct DefaultTechnologyListRendererTests {
)
}

@Test("aligns table columns after removing remote control characters")
func alignsSanitizedTable() throws {
// -- Arrange --
let technologies = [
Technology(
name: "MetricKit\u{0007}", identifier: "doc://com.apple.documentation/documentation/MetricKit\u{0007}"),
Technology(name: "Swift", identifier: "doc://com.apple.documentation/documentation/Swift"),
]
let renderer = DefaultTechnologyListRenderer(output: .table)

// -- Act --
let output = try renderer.render(technologies)

// -- Assert --
#expect(
output == """
TECHNOLOGY IDENTIFIER
MetricKit doc://com.apple.documentation/documentation/MetricKit
Swift doc://com.apple.documentation/documentation/Swift
"""
)
}

@Test("renders table headers for an empty catalog")
func rendersEmptyTable() throws {
// -- Arrange --
let renderer = DefaultTechnologyListRenderer(output: .table)

// -- Act --
let output = try renderer.render([])

// -- Assert --
#expect(output == "TECHNOLOGY IDENTIFIER")
}

@Test("renders a JSON array of technology objects")
func rendersJSON() throws {
// -- Arrange --
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import Testing

@testable import CLI

@Suite("Text type documentation content rendering")
struct TextTypeDocumentationRendererContentTests {
@Suite("Human documentation content rendering")
struct HumanDocumentationRendererContentTests {
@Test("renders overview prose, inline symbols, and indented code examples")
func rendersOverview() throws {
// -- Arrange --
Expand All @@ -25,7 +25,7 @@ struct TextTypeDocumentationRendererContentTests {
)

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(output.contains("Overview\n────────\n\n Implement body using `Text`."))
Expand Down Expand Up @@ -56,7 +56,7 @@ struct TextTypeDocumentationRendererContentTests {
)

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(
Expand Down Expand Up @@ -90,7 +90,7 @@ struct TextTypeDocumentationRendererContentTests {
)

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(output.contains(" 3. Create a view.\n\n • Add a body.\n 4. Preview it."))
Expand All @@ -110,7 +110,7 @@ struct TextTypeDocumentationRendererContentTests {
)

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(output.contains(" A `View` with body and style."))
Expand All @@ -126,7 +126,7 @@ struct TextTypeDocumentationRendererContentTests {
""")

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import Testing

@testable import CLI

@Suite("Text type documentation reference rendering")
struct TextTypeDocumentationRendererReferenceTests {
@Suite("Human documentation reference rendering")
struct HumanDocumentationRendererReferenceTests {
@Test("renders the canonical documentation URL")
func rendersCanonicalURL() throws {
// -- Arrange --
Expand Down Expand Up @@ -32,7 +32,7 @@ struct TextTypeDocumentationRendererReferenceTests {
data, destination: .init(technology: "metrickit", path: "/documentation/metrickit/mxhangdiagnostic"))

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(
Expand Down Expand Up @@ -84,7 +84,7 @@ struct TextTypeDocumentationRendererReferenceTests {
technology: "swiftui", path: "/documentation/swiftui/button"))

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(output.contains(" • init(intent:label:)\n Creates a button that performs an `AppIntent`."))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import Testing

@testable import CLI

@Suite("Text type documentation renderer")
struct TextTypeDocumentationRendererTests {
@Suite("Human documentation renderer")
struct HumanDocumentationRendererTests {
@Test("renders the type summary and declaration")
func rendersSummaryAndDeclaration() throws {
// -- Arrange --
Expand Down Expand Up @@ -39,7 +39,7 @@ struct TextTypeDocumentationRendererTests {
data, destination: .init(technology: "metrickit", path: "/documentation/metrickit/mxhangdiagnostic"))

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(
Expand Down Expand Up @@ -107,7 +107,7 @@ struct TextTypeDocumentationRendererTests {
data, destination: .init(technology: "metrickit", path: "/documentation/metrickit/mxhangdiagnostic"))

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(output.contains("Deprecated\n──────────\n Use HangDiagnostic instead."))
Expand Down Expand Up @@ -139,7 +139,7 @@ struct TextTypeDocumentationRendererTests {
data, destination: .init(technology: "metrickit", path: "/documentation/metrickit/mxhangdiagnostic"))

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(
Expand Down Expand Up @@ -218,7 +218,7 @@ struct TextTypeDocumentationRendererTests {
data, destination: .init(technology: "metrickit", path: "/documentation/metrickit/mxhangdiagnostic"))

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(output.contains("Inherits From\n─────────────\n • MXDiagnostic"))
Expand Down Expand Up @@ -268,7 +268,7 @@ struct TextTypeDocumentationRendererTests {
technology: "swift", path: "/documentation/swift/string"))

// -- Act --
let output = TextTypeDocumentationRenderer().render(page)
let output = HumanDocumentationRenderer().render(DocumentationPresenter().page(page, audience: .human))

// -- Assert --
#expect(
Expand Down
Loading
Loading