feat: Working on documentation

This commit is contained in:
2024-12-06 11:06:10 -05:00
parent 9985b55f88
commit f0873d3b44
16 changed files with 326 additions and 58 deletions

8
Examples/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

24
Examples/Package.resolved Normal file
View File

@@ -0,0 +1,24 @@
{
"originHash" : "afba34e2c2164a53d5b868cb4ece6f0e542fcaed383f1b226ed9179283d0e060",
"pins" : [
{
"identity" : "rainbow",
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Rainbow",
"state" : {
"revision" : "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3",
"version" : "4.0.1"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
"version" : "1.5.0"
}
}
],
"version" : 3
}

27
Examples/Package.swift Normal file
View File

@@ -0,0 +1,27 @@
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "CliDoc-Examples",
products: [
.executable(name: "examples", targets: ["CliDoc-Examples"])
],
dependencies: [
.package(path: "../"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0")
],
targets: [
.executableTarget(
name: "CliDoc-Examples",
dependencies: [
.product(name: "CliDoc", package: "swift-cli-doc"),
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
),
.testTarget(
name: "CliDoc-ExamplesTests",
dependencies: ["CliDoc-Examples"]
)
]
)

View File

@@ -0,0 +1,14 @@
import ArgumentParser
import CliDoc
@main
struct Application: ParsableCommand {
static var configuration: CommandConfiguration {
.init(
commandName: "examples",
subcommands: [
SectionCommand.self
]
)
}
}

View File

@@ -0,0 +1,30 @@
import ArgumentParser
import CliDocCore
struct SectionCommand: ParsableCommand {
static var configuration: CommandConfiguration {
.init(commandName: "section")
}
func run() throws {
let section = Section {
"My super awesome section"
} header: {
"Awesome"
} footer: {
"Note: this is super awesome"
}
print(section.style(MySectionStyle()).render())
}
}
struct MySectionStyle: SectionStyle {
func render(content: SectionConfiguration) -> some TextNode {
VStack(separator: .newLine(count: 2)) {
content.header.color(.green).bold().underline()
content.content
content.footer.italic()
}
}
}

View File

@@ -0,0 +1,6 @@
import Testing
@testable import CliDoc_Examples
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}

3
Examples/justfile Normal file
View File

@@ -0,0 +1,3 @@
run command="section":
swift run examples {{command}}

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "ce5e40ef3be5875abe47bbfe26413215dee9b937de7df5293343757c7476d755", "originHash" : "d1c093149081cbc269ce401633fdb3414e17bc9f7c2290173f3f77bf0537c8a9",
"pins" : [ "pins" : [
{ {
"identity" : "rainbow", "identity" : "rainbow",
@@ -9,6 +9,24 @@
"revision" : "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3", "revision" : "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3",
"version" : "4.0.1" "version" : "4.0.1"
} }
},
{
"identity" : "swift-docc-plugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-plugin",
"state" : {
"revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64",
"version" : "1.4.3"
}
},
{
"identity" : "swift-docc-symbolkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-docc-symbolkit",
"state" : {
"revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
"version" : "1.0.0"
}
} }
], ],
"version" : 3 "version" : 3

View File

@@ -9,7 +9,8 @@ let package = Package(
.library(name: "CliDoc", targets: ["CliDoc"]) .library(name: "CliDoc", targets: ["CliDoc"])
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/onevcat/Rainbow", from: "4.0.0") .package(url: "https://github.com/onevcat/Rainbow", from: "4.0.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
], ],
targets: [ targets: [
.target( .target(

View File

@@ -1,8 +1,9 @@
import Rainbow import Rainbow
public struct ExampleSection<Header: TextNode, Label: TextNode>: TextNode {
public typealias Example = (label: String, example: String) public typealias Example = (label: String, example: String)
public struct ExampleSection<Header: TextNode, Label: TextNode>: TextNode {
@usableFromInline @usableFromInline
let configuration: ExampleSectionConfiguration let configuration: ExampleSectionConfiguration
@@ -40,29 +41,26 @@ public struct ExampleSection<Header: TextNode, Label: TextNode>: TextNode {
/// The type-erased configuration of an ``ExampleSection`` /// The type-erased configuration of an ``ExampleSection``
public struct ExampleSectionConfiguration { public struct ExampleSectionConfiguration {
@usableFromInline public let title: any TextNode
let title: any TextNode
public let label: any TextNode
public let examples: [Example]
@usableFromInline @usableFromInline
let label: any TextNode init(title: any TextNode, label: any TextNode, examples: [Example]) {
@usableFromInline
let examples: [ExampleSection.Example]
@usableFromInline
init(title: any TextNode, label: any TextNode, examples: [ExampleSection.Example]) {
self.title = title self.title = title
self.label = label self.label = label
self.examples = examples self.examples = examples
} }
} }
/// The configuration context for the `examples` of an ``ExampleSection``.
public struct ExampleConfiguration { public struct ExampleConfiguration {
@usableFromInline public let examples: [Example]
let examples: [ExampleSection.Example]
@usableFromInline @usableFromInline
init(examples: [ExampleSection.Example]) { init(examples: [Example]) {
self.examples = examples self.examples = examples
} }
} }
@@ -85,7 +83,7 @@ public extension ExampleSection {
} }
} }
extension Array where Element == ExampleSection.Example { extension Array where Element == Example {
@inlinable @inlinable
func exampleStyle<S: ExampleStyle>(_ style: S) -> some TextNode { func exampleStyle<S: ExampleStyle>(_ style: S) -> some TextNode {
style.render(content: .init(examples: self)) style.render(content: .init(examples: self))
@@ -152,8 +150,8 @@ public struct DefaultExampleStyle: ExampleStyle {
VStack(separator: .newLine(count: 2)) { VStack(separator: .newLine(count: 2)) {
content.examples.map { example in content.examples.map { example in
VStack { VStack {
Label(example.label.green.bold) example.label.color(.green).bold()
ShellCommand { example.example }.style(.default) ShellCommand(example.example).style(.default)
} }
} }
} }

View File

@@ -1,3 +1,4 @@
import CliDocCore
import Rainbow import Rainbow
public struct Note<Label: TextNode, Content: TextNode>: TextNode { public struct Note<Label: TextNode, Content: TextNode>: TextNode {
@@ -18,7 +19,7 @@ public struct Note<Label: TextNode, Content: TextNode>: TextNode {
@inlinable @inlinable
public var body: some TextNode { public var body: some TextNode {
noteStyle(.default) style(.default)
} }
} }
@@ -26,11 +27,10 @@ public extension Note where Label == String {
@inlinable @inlinable
init( init(
_ label: String = "NOTE:", _ label: @autoclosure () -> String = "NOTE:",
@TextBuilder content: () -> Content @TextBuilder content: () -> Content
) { ) {
self.label = label self.init(label, content: content)
self.content = content()
} }
static func important( static func important(
@@ -48,6 +48,7 @@ public extension Note where Label == String {
} }
} }
// TODO: Remove the important and see also.
public extension Note where Label == String, Content == String { public extension Note where Label == String, Content == String {
@inlinable @inlinable
@@ -74,28 +75,39 @@ public extension Note where Label == String, Content == String {
} }
public struct NoteStyleConfiguration { public struct NoteStyleConfiguration {
@usableFromInline
let label: any TextNode let label: any TextNode
@usableFromInline
let content: any TextNode let content: any TextNode
@usableFromInline
init(label: any TextNode, content: any TextNode) {
self.label = label
self.content = content
}
} }
public extension Note { public extension Note {
func noteStyle<S: NoteStyleModifier>(_ modifier: S) -> some TextNode { @inlinable
modifier.render(content: .init(label: label, content: content)) func style<S: NoteStyle>(_ modifier: S) -> some TextNode {
modifier.render(content: NoteStyleConfiguration(label: label, content: content))
} }
} }
// MARK: - Style // MARK: - Style
public protocol NoteStyleModifier: TextModifier where Content == NoteStyleConfiguration {} public protocol NoteStyle: TextModifier where Content == NoteStyleConfiguration {}
public extension NoteStyleModifier where Self == DefaultNoteStyle { public extension NoteStyle where Self == DefaultNoteStyle {
static var `default`: Self { static var `default`: Self {
DefaultNoteStyle() DefaultNoteStyle()
} }
} }
public struct DefaultNoteStyle: NoteStyleModifier { public struct DefaultNoteStyle: NoteStyle {
@inlinable
public func render(content: NoteStyleConfiguration) -> some TextNode { public func render(content: NoteStyleConfiguration) -> some TextNode {
HStack { HStack {
content.label.color(.yellow).textStyle(.bold) content.label.color(.yellow).textStyle(.bold)

View File

@@ -1,20 +1,43 @@
import CliDocCore
import Rainbow import Rainbow
public struct ShellCommand<Content: TextNode>: TextNode { /// Represents a shell command text node, with a symbol and the content of
/// the command. Used for displaying example shell commands.
///
///
public struct ShellCommand<Content: TextNode, Symbol: TextNode>: TextNode {
@usableFromInline @usableFromInline
var symbol: any TextNode let symbol: Symbol
@usableFromInline @usableFromInline
var content: Content let content: Content
/// Create a new shell command with the given content and symbol.
///
/// - Parameters:
/// - content: The shell command to display.
/// - symbol: The symbol to use in front of the shell command.
@inlinable @inlinable
public init( public init(
symbol: any TextNode = "$", @TextBuilder content: () -> Content,
@TextBuilder symbol: () -> Symbol
) {
self.symbol = symbol()
self.content = content()
}
/// Create a new shell command with the given content and symbol.
///
/// - Parameters:
/// - symbol: The symbol to use in front of the shell command.
/// - content: The shell command to display.
@inlinable
public init(
symbol: @autoclosure () -> Symbol,
@TextBuilder content: () -> Content @TextBuilder content: () -> Content
) { ) {
self.symbol = symbol self.init(content: content, symbol: symbol)
self.content = content()
} }
@inlinable @inlinable
@@ -23,22 +46,35 @@ public struct ShellCommand<Content: TextNode>: TextNode {
} }
} }
public extension ShellCommand where Content == String { public extension ShellCommand where Content == String, Symbol == String {
/// Create a new shell command with the given content and symbol.
///
/// - Parameters:
/// - content: The shell command to display.
/// - symbol: The symbol to use in front of the shell command.
@inlinable @inlinable
init( init(
_ content: String, _ content: @autoclosure () -> String,
symbol: any TextNode = "$" symbol: @autoclosure () -> String = "$"
) { ) {
self.init(symbol: symbol) { content } self.init(content: content, symbol: symbol)
} }
} }
public struct ShellCommandConfiguration { public struct ShellCommandConfiguration {
let symbol: any TextNode public let symbol: any TextNode
let content: any TextNode
public let content: any TextNode
@usableFromInline
init(symbol: any TextNode, content: any TextNode) {
self.symbol = symbol
self.content = content
}
} }
public extension ShellCommand { public extension ShellCommand {
@inlinable
func style<S: ShellCommandStyle>(_ style: S) -> some TextNode { func style<S: ShellCommandStyle>(_ style: S) -> some TextNode {
style.render(content: .init(symbol: symbol, content: content)) style.render(content: .init(symbol: symbol, content: content))
} }
@@ -46,7 +82,7 @@ public extension ShellCommand {
// MARK: - Style // MARK: - Style
public protocol ShellCommandStyle: TextModifier where Self.Content == ShellCommandConfiguration {} public protocol ShellCommandStyle: TextModifier where Content == ShellCommandConfiguration {}
public extension ShellCommandStyle where Self == DefaultShellCommandStyle { public extension ShellCommandStyle where Self == DefaultShellCommandStyle {
static var `default`: Self { DefaultShellCommandStyle() } static var `default`: Self { DefaultShellCommandStyle() }
@@ -57,7 +93,7 @@ public struct DefaultShellCommandStyle: ShellCommandStyle {
public func render(content: ShellCommandConfiguration) -> some TextNode { public func render(content: ShellCommandConfiguration) -> some TextNode {
HStack { HStack {
content.symbol content.symbol
content.content.textStyle(.italic) content.content.italic()
} }
} }
} }

View File

@@ -1,4 +1,52 @@
// TODO: Add vertical spacing. /// A section of text nodes, that can contain a header, content, and footer.
///
/// This allows nodes to be grouped and styled together.
///
/// **Example:**
///
/// ```swift
/// let mySection = Section {
/// "My super awesome section content"
/// } header: {
/// "Awesome"
/// } footer: {
/// "Note: this is super awesome".italic()
/// }
/// ```
///
/// **Styling Sections:**
///
/// You can style a section by creating a custom ``SectionStyle``, which gives you the
/// opportunity to arrange and style the nodes within the section.
///
/// ```swift
/// struct MySectionStyle: SectionStyle {
/// func render(content: SectionConfiguration) -> some TextNode {
/// VStack(separator: .newLine(count: 2)) {
/// content.header
/// .color(.green)
/// .bold()
/// .underline()
/// content.content
/// content.footer.italic()
/// }
/// }
/// }
///
/// mySection.style(MySectionStyle())
///
/// print(mySection.render())
/// ```
/// **Note:** colored output / styling only shows in the terminal.
///
/// ```bash
///
/// Awesome
///
/// My super awesome section
///
/// Note: this is super awesome
/// ```
public struct Section<Header: TextNode, Content: TextNode, Footer: TextNode>: TextNode { public struct Section<Header: TextNode, Content: TextNode, Footer: TextNode>: TextNode {
@usableFromInline @usableFromInline
@@ -10,6 +58,12 @@ public struct Section<Header: TextNode, Content: TextNode, Footer: TextNode>: Te
@usableFromInline @usableFromInline
let footer: Footer let footer: Footer
/// Create a new section with the given content, header, and footer.
///
/// - Parameters:
/// - content: The content of the section.
/// - header: The header for the section.
/// - footer: The footer for the section.
@inlinable @inlinable
public init( public init(
@TextBuilder content: () -> Content, @TextBuilder content: () -> Content,
@@ -27,6 +81,11 @@ public struct Section<Header: TextNode, Content: TextNode, Footer: TextNode>: Te
} }
public extension Section where Footer == Empty { public extension Section where Footer == Empty {
/// Create a new section with the given content and header, with no footer.
///
/// - Parameters:
/// - content: The content of the section.
/// - header: The header for the section.
@inlinable @inlinable
init( init(
@TextBuilder content: () -> Content, @TextBuilder content: () -> Content,
@@ -37,6 +96,11 @@ public extension Section where Footer == Empty {
} }
public extension Section where Header == Empty { public extension Section where Header == Empty {
/// Create a new section with the given content and footer, with no header.
///
/// - Parameters:
/// - content: The content of the section.
/// - footer: The footer for the section.
@inlinable @inlinable
init( init(
@TextBuilder content: () -> Content, @TextBuilder content: () -> Content,
@@ -46,28 +110,29 @@ public extension Section where Header == Empty {
} }
} }
public extension Section where Header == Empty, Footer == Empty {
@inlinable
init(
@TextBuilder content: () -> Content
) {
self.init(content: content, header: { Empty() }, footer: { Empty() })
}
}
// MARK: - Style // MARK: - Style
public extension Section { public extension Section {
/// Style a ``Section`` using the given ``SectionStyle``.
///
/// - Parameters:
/// - style: The section style to use.
@inlinable @inlinable
func style<S: SectionStyle>(_ style: S) -> some TextNode { func style<S: SectionStyle>(_ style: S) -> some TextNode {
style.render(content: .init(header: header, content: content, footer: footer)) style.render(content: .init(header: header, content: content, footer: footer))
} }
} }
/// Holds the type-erased values of a ``Section``, used to style a section.
public struct SectionConfiguration { public struct SectionConfiguration {
/// The type-erased header of a section.
public let header: any TextNode public let header: any TextNode
/// The type-erased content of a section.
public let content: any TextNode public let content: any TextNode
/// The type-erased footer of a section.
public let footer: any TextNode public let footer: any TextNode
@usableFromInline @usableFromInline
@@ -81,13 +146,33 @@ public struct SectionConfiguration {
public protocol SectionStyle: TextModifier where Content == SectionConfiguration {} public protocol SectionStyle: TextModifier where Content == SectionConfiguration {}
public extension SectionStyle where Self == DefaultSectionStyle { public extension SectionStyle where Self == DefaultSectionStyle {
static var `default`: Self { DefaultSectionStyle() }
/// Style a section using the default style, separating the content with
/// a new line between the elements.
static var `default`: Self { `default`(separator: .newLine(count: 2)) }
/// Style a section using the default style, separating the content with
/// given separator between the elements.
///
/// - Parameters:
/// - separator: The separator to use to separate elements in a section.
static func `default`(separator: Separator.Vertical) -> Self {
DefaultSectionStyle(separator: separator)
}
} }
/// Represents the default ``SectionStyle``, which arranges the nodes in
/// a ``VStack``, using the separator passed in.
///
/// - SeeAlso: ``SectionStyle/default(separator:)``
///
public struct DefaultSectionStyle: SectionStyle { public struct DefaultSectionStyle: SectionStyle {
@usableFromInline
let separator: Separator.Vertical
public func render(content: SectionConfiguration) -> some TextNode { public func render(content: SectionConfiguration) -> some TextNode {
VStack(separator: .newLine(count: 2)) { VStack(separator: separator) {
content.header content.header
content.content content.content
content.footer content.footer

View File

@@ -62,8 +62,7 @@ public extension TextNode {
public protocol TextStyle: TextModifier where Content == TextStyleConfiguration {} public protocol TextStyle: TextModifier where Content == TextStyleConfiguration {}
public struct TextStyleConfiguration { public struct TextStyleConfiguration {
@usableFromInline public let node: any TextNode
let node: any TextNode
@usableFromInline @usableFromInline
init(_ node: any TextNode) { init(_ node: any TextNode) {

View File

@@ -58,10 +58,10 @@ struct CliDocTests {
\("Examples:".applyingStyle(.bold).applyingColor(.yellow)) \("Some common usage examples.".italic) \("Examples:".applyingStyle(.bold).applyingColor(.yellow)) \("Some common usage examples.".italic)
\("First".red) \("First".red)
$ \("ls -lah".italic) > \("ls -lah".italic)
\("Second".red) \("Second".red)
$ \("find . -name foo".italic) > \("find . -name foo".italic)
""" """
let result = printIfNotEqual( let result = printIfNotEqual(
examples.exampleStyle(CustomExampleOnlyStyle()).render(), examples.exampleStyle(CustomExampleOnlyStyle()).render(),
@@ -99,7 +99,7 @@ struct CustomExampleOnlyStyle: ExampleStyle {
content.examples.map { example in content.examples.map { example in
VStack { VStack {
example.label.red example.label.red
ShellCommand { example.example } ShellCommand(symbol: ">") { example.example }
} }
} }
} }

7
justfile Normal file
View File

@@ -0,0 +1,7 @@
preview-documentation target="CliDoc":
swift package \
--disable-sandbox \
preview-documentation \
--target {{target}} \
--include-extended-types