diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..24a8e87 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 3d9d43e..adfc5fe 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc .nvim/* +docs/* diff --git a/Package.resolved b/Package.resolved index 1f848a9..5c6e720 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "d1c093149081cbc269ce401633fdb3414e17bc9f7c2290173f3f77bf0537c8a9", + "originHash" : "d5e07b58f04c94c37ba09a0f1c9d09fc24e97c047c48b2c5a4a573fcda7f6c98", "pins" : [ { "identity" : "rainbow", @@ -10,6 +10,15 @@ "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" + } + }, { "identity" : "swift-docc-plugin", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 2dafdfb..27b1b7d 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,8 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/onevcat/Rainbow", from: "4.0.0"), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0") ], targets: [ .target( @@ -27,6 +28,7 @@ let package = Package( name: "CliDoc", dependencies: [ "CliDocCore", + .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "Rainbow", package: "Rainbow") ] ), diff --git a/Sources/CliDoc/CommandConfiguration+TextNode.swift b/Sources/CliDoc/CommandConfiguration+TextNode.swift new file mode 100644 index 0000000..291685c --- /dev/null +++ b/Sources/CliDoc/CommandConfiguration+TextNode.swift @@ -0,0 +1,36 @@ +import ArgumentParser + +public extension CommandConfiguration { + + /// Generate a new command configuration, using ``TextNode``'s for the abstract, + /// usage, and discussion parameters. + /// + /// + init( + commandName: String? = nil, + abstract: Abstract, + usage: Usage, + discussion: Discussion, + version: String = "", + shouldDisplay: Bool = true, + subcommands ungroupedSubcommands: [ParsableCommand.Type] = [], + groupedSubcommands: [CommandGroup] = [], + defaultSubcommand: ParsableCommand.Type? = nil, + helpNames: NameSpecification? = nil, + aliases: [String] = [] + ) { + self.init( + commandName: commandName, + abstract: abstract.render(), + usage: usage.render(), + discussion: discussion.render(), + version: version, + shouldDisplay: shouldDisplay, + subcommands: ungroupedSubcommands, + groupedSubcommands: groupedSubcommands, + defaultSubcommand: defaultSubcommand, + helpNames: helpNames, + aliases: aliases + ) + } +} diff --git a/Sources/CliDoc/Nodes/Abstract.swift b/Sources/CliDoc/Nodes/Abstract.swift new file mode 100644 index 0000000..66b83a1 --- /dev/null +++ b/Sources/CliDoc/Nodes/Abstract.swift @@ -0,0 +1,14 @@ +import CliDocCore + +public struct Abstract: TextNode { + @usableFromInline + let content: Content + + public init(@TextBuilder content: () -> Content) { + self.content = content() + } + + public var body: some TextNode { + content + } +} diff --git a/Sources/CliDoc/Nodes/Discussion.swift b/Sources/CliDoc/Nodes/Discussion.swift new file mode 100644 index 0000000..5803502 --- /dev/null +++ b/Sources/CliDoc/Nodes/Discussion.swift @@ -0,0 +1,14 @@ +import CliDocCore + +public struct Discussion: TextNode { + @usableFromInline + let content: Content + + public init(@TextBuilder content: () -> Content) { + self.content = content() + } + + public var body: some TextNode { + content + } +} diff --git a/Sources/CliDoc/Nodes/Note.swift b/Sources/CliDoc/Nodes/Note.swift index 44f8d0c..4e9412f 100644 --- a/Sources/CliDoc/Nodes/Note.swift +++ b/Sources/CliDoc/Nodes/Note.swift @@ -1,6 +1,7 @@ import CliDocCore import Rainbow +// TODO: Use labeled content. public struct Note: TextNode { @usableFromInline let label: Label diff --git a/Sources/CliDoc/Nodes/Usage.swift b/Sources/CliDoc/Nodes/Usage.swift new file mode 100644 index 0000000..884da08 --- /dev/null +++ b/Sources/CliDoc/Nodes/Usage.swift @@ -0,0 +1,14 @@ +import CliDocCore + +public struct Usage: TextNode { + @usableFromInline + let content: Content + + public init(@TextBuilder content: () -> Content) { + self.content = content() + } + + public var body: some TextNode { + content + } +} diff --git a/Sources/CliDocCore/Documentation.docc/Resources/section.png b/Sources/CliDocCore/Documentation.docc/Resources/section.png index 631346e..4851e80 100644 Binary files a/Sources/CliDocCore/Documentation.docc/Resources/section.png and b/Sources/CliDocCore/Documentation.docc/Resources/section.png differ diff --git a/Sources/CliDocCore/Nodes/LabeledContent.swift b/Sources/CliDocCore/Nodes/LabeledContent.swift new file mode 100644 index 0000000..fec35bc --- /dev/null +++ b/Sources/CliDocCore/Nodes/LabeledContent.swift @@ -0,0 +1,114 @@ +/// A text node that consists of a label and content. +/// +/// +public struct LabeledContent: TextNode { + + @usableFromInline + let label: Label + + @usableFromInline + let content: Content + + @inlinable + public init( + @TextBuilder _ content: () -> Content, + @TextBuilder label: () -> Label + ) { + self.label = label() + self.content = content() + } + + @inlinable + public var body: some TextNode { + style(.default) + } +} + +public extension LabeledContent { + + /// Apply the given style to the labeled content. + /// + /// - Parameters: + /// - style: The labeled content style to apply. + @inlinable + func style(_ style: S) -> some TextNode { + style.render(content: .init(label: label, content: content)) + } +} + +/// Holds the type-erased label and content of a ``LabeledContent`` text node. +/// +/// This is used when creating custom styles for the ``LabeledContent``. +/// +public struct LabeledContentConfiguration { + + /// The type-erased label text node. + public let label: any TextNode + + /// The type-erased content text node. + public let content: any TextNode + + @usableFromInline + init(label: any TextNode, content: any TextNode) { + self.label = label + self.content = content + } +} + +public protocol LabeledContentStyle: TextModifier where Content == LabeledContentConfiguration {} + +public extension LabeledContentStyle where Self == HorizontalLabeledContentStyle { + + static var `default`: Self { + horizontal() + } + + @inlinable + static func horizontal(separator: Separator.Horizontal = .space()) -> Self { + HorizontalLabeledContentStyle(separator: separator) + } +} + +public extension LabeledContentStyle where Self == VerticalLabeledContentStyle { + + @inlinable + static func vertical(separator: Separator.Vertical = .newLine()) -> Self { + VerticalLabeledContentStyle(separator: separator) + } +} + +public struct HorizontalLabeledContentStyle: LabeledContentStyle { + + @usableFromInline + let separator: Separator.Horizontal + + @usableFromInline + init(separator: Separator.Horizontal) { + self.separator = separator + } + + public func render(content: LabeledContentConfiguration) -> some TextNode { + HStack(separator: separator) { + content.label + content.content + } + } +} + +public struct VerticalLabeledContentStyle: LabeledContentStyle { + + @usableFromInline + let separator: Separator.Vertical + + @usableFromInline + init(separator: Separator.Vertical) { + self.separator = separator + } + + public func render(content: LabeledContentConfiguration) -> some TextNode { + VStack(separator: separator) { + content.label + content.content + } + } +} diff --git a/Tests/CliDocCoreTests/CliDocCoreTests.swift b/Tests/CliDocCoreTests/CliDocCoreTests.swift index 8ce5dea..bf3cb8a 100644 --- a/Tests/CliDocCoreTests/CliDocCoreTests.swift +++ b/Tests/CliDocCoreTests/CliDocCoreTests.swift @@ -86,11 +86,30 @@ struct CliDocCoreTests { #expect(array.render() == "foo bar") } + @Test + func testLabeledContent() { + let horizontal = LabeledContent { + "Content" + } label: { + "Label:".color(.yellow).bold() + } + + let expected = """ + \("Label:".yellow.bold) Content + """ + #expect(horizontal.render() == expected) + + #expect(horizontal.style(.vertical()).render() == """ + \("Label:".yellow.bold) + Content + """) + } + @Test(arguments: [ Style.bold, .italic, .dim, .underline, .blink, .strikethrough ]) func testTextStyles(style: Style) { - let node = Group { "foo" }.textStyle(StyledText(style)) + let node = Group { "foo" }.textStyle(_StyledText(style)) let string = "foo".applyingStyle(style) #expect(node.render() == string) } diff --git a/justfile b/justfile index b7ecd9e..d5b903f 100644 --- a/justfile +++ b/justfile @@ -1,8 +1,28 @@ +[private] +default: + @just --list + +clean: + @rm -rf .build + preview-documentation target="CliDoc": - swift package \ + # using the --enable-experimental-combined-documentation doesn't work in previews currently. + @swift package \ --disable-sandbox \ + --allow-writing-to-directory "docs/" \ preview-documentation \ --target {{target}} \ --include-extended-types \ - --enable-inherited-docs + --enable-inherited-docs \ + +build-documentation: + swift package \ + --disable-sandbox \ + --allow-writing-to-directory "docs/" \ + generate-documentation \ + --target CliDoc \ + --target CliDocCore \ + --output-path "docs/" \ + --transform-for-static-hosting \ + --enable-experimental-combined-documentation