feat: Adds stack separators, removes some unused nodes from cli-doc module
All checks were successful
CI / Run tests. (push) Successful in 52s

This commit is contained in:
2024-12-08 10:58:03 -05:00
parent c977a1c805
commit c6a269f062
12 changed files with 229 additions and 177 deletions

View File

@@ -33,4 +33,66 @@ public extension CommandConfiguration {
aliases: aliases
)
}
/// Generate a new command configuration, using ``TextNode``'s for the usage, and discussion parameters.
///
///
init<U: TextNode, D: TextNode>(
commandName: String? = nil,
abstract: String = "",
usage: Usage<U>,
discussion: Discussion<D>,
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<D: TextNode>(
commandName: String? = nil,
abstract: String = "",
usage: String? = nil,
discussion: Discussion<D>,
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
)
}
}

View File

@@ -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))
}
}

View File

@@ -1,19 +0,0 @@
public struct Label<Content: TextNode>: 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
}
}

View File

@@ -1,118 +0,0 @@
import CliDocCore
import Rainbow
// TODO: Use labeled content.
public struct Note<Label: TextNode, Content: TextNode>: 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<S: NoteStyle>(_ 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
}
}
}

View File

@@ -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<S: HStackStyle>(_ 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)
}
}

View File

@@ -9,6 +9,11 @@ public struct LabeledContent<Label: TextNode, Content: TextNode>: 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)
}
}

View File

@@ -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))
}
}

View File

@@ -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")

View File

@@ -0,0 +1,29 @@
/// Represents the content of an ``HStack`` or a ``VStack``.
///
///
public struct StackConfiguration {
public let content: [any TextNode]
}
@usableFromInline
struct AnySeparatableStackNode<Separator: TextNode>: 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())
}
}

View File

@@ -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<S: VStackStyle>(_ 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)
}
}

View File

@@ -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

View File

@@ -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<CustomExa
struct CustomExampleOnlyStyle: ExampleStyle {
func render(content: ExampleConfiguration) -> 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))
}
}