diff --git a/Sources/CliDoc/CommandConfiguration+TextNode.swift b/Sources/CliDoc/CommandConfiguration+TextNode.swift index 291685c..fe8745d 100644 --- a/Sources/CliDoc/CommandConfiguration+TextNode.swift +++ b/Sources/CliDoc/CommandConfiguration+TextNode.swift @@ -33,4 +33,66 @@ public extension CommandConfiguration { aliases: aliases ) } + + /// Generate a new command configuration, using ``TextNode``'s for the usage, and discussion parameters. + /// + /// + init( + commandName: String? = nil, + abstract: String = "", + 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, + usage: usage.render(), + discussion: discussion.render(), + version: version, + shouldDisplay: shouldDisplay, + subcommands: ungroupedSubcommands, + groupedSubcommands: groupedSubcommands, + defaultSubcommand: defaultSubcommand, + helpNames: helpNames, + aliases: aliases + ) + } + + /// Generate a new command configuration, using ``TextNode``'s for the discussion parameter. + /// + /// + init( + commandName: String? = nil, + abstract: String = "", + usage: String? = nil, + 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, + usage: usage, + discussion: discussion.render(), + version: version, + shouldDisplay: shouldDisplay, + subcommands: ungroupedSubcommands, + groupedSubcommands: groupedSubcommands, + defaultSubcommand: defaultSubcommand, + helpNames: helpNames, + aliases: aliases + ) + } } diff --git a/Sources/CliDoc/Nodes/ExampleSection.swift b/Sources/CliDoc/Nodes/ExampleSection.swift index 01339f1..7765276 100644 --- a/Sources/CliDoc/Nodes/ExampleSection.swift +++ b/Sources/CliDoc/Nodes/ExampleSection.swift @@ -147,7 +147,7 @@ public struct DefaultExampleStyle: ExampleStyle { @inlinable public func render(content: ExampleConfiguration) -> some TextNode { - VStack(separator: .newLine(count: 2)) { + VStack { content.examples.map { example in VStack { example.label.color(.green).bold() @@ -155,5 +155,6 @@ public struct DefaultExampleStyle: ExampleStyle { } } } + .separator(.newLine(count: 2)) } } diff --git a/Sources/CliDoc/Nodes/Label.swift b/Sources/CliDoc/Nodes/Label.swift deleted file mode 100644 index 34cd5e6..0000000 --- a/Sources/CliDoc/Nodes/Label.swift +++ /dev/null @@ -1,19 +0,0 @@ -public struct Label: TextNode { - @usableFromInline - let content: Content - - @inlinable - public init(@TextBuilder _ content: () -> Content) { - self.content = content() - } - - @inlinable - public init(_ content: Content) { - self.content = content - } - - @inlinable - public var body: some TextNode { - content - } -} diff --git a/Sources/CliDoc/Nodes/Note.swift b/Sources/CliDoc/Nodes/Note.swift deleted file mode 100644 index 4e9412f..0000000 --- a/Sources/CliDoc/Nodes/Note.swift +++ /dev/null @@ -1,118 +0,0 @@ -import CliDocCore -import Rainbow - -// TODO: Use labeled content. -public struct Note: TextNode { - @usableFromInline - let label: Label - - @usableFromInline - let content: Content - - @inlinable - public init( - @TextBuilder _ label: () -> Label, - @TextBuilder content: () -> Content - ) { - self.label = label() - self.content = content() - } - - @inlinable - public var body: some TextNode { - style(.default) - } -} - -public extension Note where Label == String { - - @inlinable - init( - _ label: @autoclosure () -> String = "NOTE:", - @TextBuilder content: () -> Content - ) { - self.init(label, content: content) - } - - static func important( - _ label: String = "IMPORTANT NOTE:", - @TextBuilder content: () -> Content - ) -> Self { - self.init(label, content: content) - } - - static func seeAlso( - _ label: String = "SEE ALSO:", - @TextBuilder content: () -> Content - ) -> Self { - self.init(label, content: content) - } -} - -// TODO: Remove the important and see also. -public extension Note where Label == String, Content == String { - - @inlinable - init( - _ label: String = "NOTE:", - content: String - ) { - self.init(label) { content } - } - - static func important( - _ label: String = "IMPORTANT NOTE:", - content: String - ) -> Self { - self.init(label, content: content) - } - - static func seeAlso( - _ label: String = "SEE ALSO:", - content: String - ) -> Self { - self.init(label, content: content) - } -} - -public struct NoteStyleConfiguration { - @usableFromInline - let label: any TextNode - - @usableFromInline - let content: any TextNode - - @usableFromInline - init(label: any TextNode, content: any TextNode) { - self.label = label - self.content = content - } -} - -public extension Note { - @inlinable - func style(_ modifier: S) -> some TextNode { - modifier.render(content: NoteStyleConfiguration(label: label, content: content)) - } -} - -// MARK: - Style - -public protocol NoteStyle: TextModifier where Content == NoteStyleConfiguration {} - -public extension NoteStyle where Self == DefaultNoteStyle { - static var `default`: Self { - DefaultNoteStyle() - } -} - -public struct DefaultNoteStyle: NoteStyle { - - @inlinable - public func render(content: NoteStyleConfiguration) -> some TextNode { - HStack { - content.label.color(.yellow).textStyle(.bold) - content.content - } - } -} diff --git a/Sources/CliDocCore/Nodes/HStack.swift b/Sources/CliDocCore/Nodes/HStack.swift index c5fe877..de53b26 100644 --- a/Sources/CliDocCore/Nodes/HStack.swift +++ b/Sources/CliDocCore/Nodes/HStack.swift @@ -4,21 +4,53 @@ public struct HStack: TextNode { @usableFromInline let content: [any TextNode] - @usableFromInline - let separator: Separator.Horizontal - @inlinable public init( - separator: Separator.Horizontal = .space(count: 1), @TextBuilder content: () -> any TextNode ) { self.content = array(from: content()) - self.separator = separator } @inlinable public var body: some TextNode { - content.removingEmptys() - .joined(separator: separator.render()) + style(.separator(.space())) + } +} + +public extension HStack { + + func style(_ style: S) -> some TextNode { + style.render(content: .init(content: content)) + } + + func separator(_ separator: Separator.Horizontal) -> some TextNode { + style(.separator(separator)) + } +} + +// MARK: - Style + +public protocol HStackStyle: TextModifier where Content == StackConfiguration {} + +public extension HStackStyle where Self == HStackSeparatorStyle { + + static func separator(_ separator: Separator.Horizontal) -> Self { + HStackSeparatorStyle(separator: separator) + } + +} + +public struct HStackSeparatorStyle: HStackStyle { + @usableFromInline + let separator: Separator.Horizontal + + @usableFromInline + init(separator: Separator.Horizontal) { + self.separator = separator + } + + @inlinable + public func render(content: StackConfiguration) -> some TextNode { + AnySeparatableStackNode(content: content, separator: separator) } } diff --git a/Sources/CliDocCore/Nodes/LabeledContent.swift b/Sources/CliDocCore/Nodes/LabeledContent.swift index fec35bc..aa23b16 100644 --- a/Sources/CliDocCore/Nodes/LabeledContent.swift +++ b/Sources/CliDocCore/Nodes/LabeledContent.swift @@ -9,6 +9,11 @@ public struct LabeledContent: TextNode { @usableFromInline let content: Content + /// Create a new labeled content text node. + /// + /// - Parameters: + /// - content: The content portion of the labeled content. + /// - label: The label for the content. @inlinable public init( @TextBuilder _ content: () -> Content, @@ -55,14 +60,27 @@ public struct LabeledContentConfiguration { } } +// MARK: - Style + +/// Represents a style for ``LabeledContent``. +/// +/// public protocol LabeledContentStyle: TextModifier where Content == LabeledContentConfiguration {} public extension LabeledContentStyle where Self == HorizontalLabeledContentStyle { + /// The default labeled content style, which places the label + /// and content inline with a space as a separator. + /// static var `default`: Self { horizontal() } + /// A horizontal labeled content style, which places the label + /// and content inline with the given separator. + /// + /// - Parameters: + /// - separator: The horizontal separator to use. @inlinable static func horizontal(separator: Separator.Horizontal = .space()) -> Self { HorizontalLabeledContentStyle(separator: separator) @@ -70,13 +88,22 @@ public extension LabeledContentStyle where Self == HorizontalLabeledContentStyle } public extension LabeledContentStyle where Self == VerticalLabeledContentStyle { - + /// A vertical labeled content style, which places the label + /// and content with the given vertical separator. + /// + /// - Parameters: + /// - separator: The vertical separator to use. @inlinable static func vertical(separator: Separator.Vertical = .newLine()) -> Self { VerticalLabeledContentStyle(separator: separator) } } +/// A labeled content style which places items inline based on a given +/// horizontal separator. +/// +/// - See Also: ``LabeledContentStyle/horizontal(separator:)`` +/// public struct HorizontalLabeledContentStyle: LabeledContentStyle { @usableFromInline @@ -88,13 +115,19 @@ public struct HorizontalLabeledContentStyle: LabeledContentStyle { } public func render(content: LabeledContentConfiguration) -> some TextNode { - HStack(separator: separator) { + HStack { content.label content.content } + .separator(separator) } } +/// A labeled content style which places items based on a given +/// vertical separator. +/// +/// - See Also: ``LabeledContentStyle/vertical(separator:)`` +/// public struct VerticalLabeledContentStyle: LabeledContentStyle { @usableFromInline @@ -106,9 +139,10 @@ public struct VerticalLabeledContentStyle: LabeledContentStyle { } public func render(content: LabeledContentConfiguration) -> some TextNode { - VStack(separator: separator) { + VStack { content.label content.content } + .separator(separator) } } diff --git a/Sources/CliDocCore/Nodes/Section.swift b/Sources/CliDocCore/Nodes/Section.swift index fb09668..bda73f2 100644 --- a/Sources/CliDocCore/Nodes/Section.swift +++ b/Sources/CliDocCore/Nodes/Section.swift @@ -171,10 +171,11 @@ public struct DefaultSectionStyle: SectionStyle { let separator: Separator.Vertical public func render(content: SectionConfiguration) -> some TextNode { - VStack(separator: separator) { + VStack { content.header content.content content.footer } + .style(.separator(separator)) } } diff --git a/Sources/CliDocCore/Nodes/Separator.swift b/Sources/CliDocCore/Nodes/Separator.swift index b6fea09..ced6245 100644 --- a/Sources/CliDocCore/Nodes/Separator.swift +++ b/Sources/CliDocCore/Nodes/Separator.swift @@ -39,11 +39,11 @@ public enum Separator { public var body: some TextNode { switch self { case let .tab(count: count): - seperator("\t", count: count) + makeSeperator("\t", count: count) case let .space(count: count): - seperator(" ", count: count) + makeSeperator(" ", count: count) case let .custom(string, count: count): - seperator(string, count: count) + makeSeperator(string, count: count) } } } @@ -85,15 +85,17 @@ public enum Separator { public var body: some TextNode { switch self { case let .newLine(count: count): - seperator("\n", count: count) + makeSeperator("\n", count: count) case let .custom(string, count: count): - seperator(string, count: count) + makeSeperator(string, count: count) } } } } +// MARK: - Private Helpers. + @usableFromInline func ensuredCount(_ count: Int) -> Int { guard count >= 1 else { return 1 } @@ -101,7 +103,7 @@ func ensuredCount(_ count: Int) -> Int { } @usableFromInline -func seperator(_ separator: String, count: Int) -> some TextNode { +func makeSeperator(_ separator: String, count: Int) -> some TextNode { let count = ensuredCount(count) assert(count >= 1, "Invalid count while creating a separator") diff --git a/Sources/CliDocCore/Nodes/StackConfiguration.swift b/Sources/CliDocCore/Nodes/StackConfiguration.swift new file mode 100644 index 0000000..e55fb7f --- /dev/null +++ b/Sources/CliDocCore/Nodes/StackConfiguration.swift @@ -0,0 +1,29 @@ +/// Represents the content of an ``HStack`` or a ``VStack``. +/// +/// +public struct StackConfiguration { + public let content: [any TextNode] +} + +@usableFromInline +struct AnySeparatableStackNode: TextNode { + + @usableFromInline + let content: [any TextNode] + + @usableFromInline + let separator: Separator + + @usableFromInline + init(content: StackConfiguration, separator: Separator) { + self.content = content.content + self.separator = separator + } + + @usableFromInline + var body: some TextNode { + content.removingEmptys() + .map { $0.render() } + .joined(separator: separator.render()) + } +} diff --git a/Sources/CliDocCore/Nodes/VStack.swift b/Sources/CliDocCore/Nodes/VStack.swift index 137ecc4..c3162c8 100644 --- a/Sources/CliDocCore/Nodes/VStack.swift +++ b/Sources/CliDocCore/Nodes/VStack.swift @@ -1,26 +1,57 @@ /// A vertical stack of text nodes. /// /// - public struct VStack: TextNode { @usableFromInline let content: [any TextNode] - @usableFromInline - let separator: Separator.Vertical - @inlinable public init( - separator: Separator.Vertical = .newLine(count: 1), @TextBuilder content: () -> any TextNode ) { self.content = array(from: content()) - self.separator = separator } @inlinable public var body: some TextNode { - content.removingEmptys() - .joined(separator: separator.render()) + style(.separator(.newLine(count: 1))) + } +} + +public extension VStack { + + func style(_ style: S) -> some TextNode { + style.render(content: .init(content: content.removingEmptys())) + } + + func separator(_ separator: Separator.Vertical) -> some TextNode { + style(.separator(separator)) + } +} + +// MARK: - Style + +public protocol VStackStyle: TextModifier where Content == StackConfiguration {} + +public extension VStackStyle where Self == VStackSeparatorStyle { + + static func separator(_ separator: Separator.Vertical) -> Self { + VStackSeparatorStyle(separator: separator) + } + +} + +public struct VStackSeparatorStyle: VStackStyle { + @usableFromInline + let separator: Separator.Vertical + + @usableFromInline + init(separator: Separator.Vertical) { + self.separator = separator + } + + @inlinable + public func render(content: StackConfiguration) -> some TextNode { + AnySeparatableStackNode(content: content, separator: separator) } } diff --git a/Tests/CliDocCoreTests/CliDocCoreTests.swift b/Tests/CliDocCoreTests/CliDocCoreTests.swift index bf3cb8a..7b16c7d 100644 --- a/Tests/CliDocCoreTests/CliDocCoreTests.swift +++ b/Tests/CliDocCoreTests/CliDocCoreTests.swift @@ -33,16 +33,20 @@ struct CliDocCoreTests { } #expect(stack.render() == "foo bar") - let tabStack = HStack(separator: .tab()) { + let tabStack = HStack { "foo" "bar" } + .separator(.tab()) + #expect(tabStack.render() == "foo\tbar") - let customStack = HStack(separator: .custom(":blob:")) { + let customStack = HStack { "foo" "bar" } + .separator(.custom(":blob:")) + #expect(customStack.render() == "foo:blob:bar") } @@ -58,10 +62,12 @@ struct CliDocCoreTests { bar """) - let customStack = VStack(separator: .custom("\n\t")) { + let customStack = VStack { "foo" "bar" } + .separator(.custom("\n\t")) + #expect(customStack.render() == """ foo \tbar diff --git a/Tests/CliDocTests/CliDocTests.swift b/Tests/CliDocTests/CliDocTests.swift index 278a1f3..4ec7be6 100644 --- a/Tests/CliDocTests/CliDocTests.swift +++ b/Tests/CliDocTests/CliDocTests.swift @@ -12,16 +12,6 @@ struct CliDocTests { return true }() - @Test - func testNote() { - #expect(setupRainbow) - let note = Note(content: "Some note.") - let expected = """ - \("NOTE:".yellow.bold) Some note. - """ - #expect(note.render() == expected) - } - @Test func testExamples() { #expect(setupRainbow) @@ -95,7 +85,7 @@ extension ExampleSectionStyle where Self == DefaultExampleSectionStyle some TextNode { - VStack(separator: .newLine(count: 2)) { + VStack { content.examples.map { example in VStack { example.label.red @@ -103,5 +93,6 @@ struct CustomExampleOnlyStyle: ExampleStyle { } } } + .separator(.newLine(count: 2)) } }