feat: Working on node builder
This commit is contained in:
@@ -38,6 +38,7 @@ let package = Package(
|
|||||||
.target(
|
.target(
|
||||||
name: "CliDoc",
|
name: "CliDoc",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
.product(name: "ShellClient", package: "swift-shell-client")
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|||||||
16
Sources/CliDoc/ArgumentParserExtensions/Abstract.swift
Normal file
16
Sources/CliDoc/ArgumentParserExtensions/Abstract.swift
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/// Use the `NodeBuilder` for generating an abstract.
|
||||||
|
public struct Abstract: NodeRepresentable {
|
||||||
|
@usableFromInline
|
||||||
|
let label: any NodeRepresentable
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(@NodeBuilder label: () -> any NodeRepresentable) {
|
||||||
|
self.label = label()
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public func render() -> String {
|
||||||
|
label.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import ArgumentParser
|
||||||
|
|
||||||
|
public extension CommandConfiguration {
|
||||||
|
|
||||||
|
init(
|
||||||
|
commandName: String? = nil,
|
||||||
|
abstract: Abstract,
|
||||||
|
usage: String? = nil,
|
||||||
|
discussion: Discussion,
|
||||||
|
version: String = "",
|
||||||
|
shouldDisplay: Bool = true,
|
||||||
|
subcommands: [any ParsableCommand.Type] = [],
|
||||||
|
groupedSubcommands: [CommandGroup] = [],
|
||||||
|
defaultSubcommand: ParsableCommand.Type? = nil,
|
||||||
|
helpNames: NameSpecification? = nil,
|
||||||
|
aliases: [String] = []
|
||||||
|
) {
|
||||||
|
self.init(
|
||||||
|
commandName: commandName,
|
||||||
|
abstract: abstract.render(),
|
||||||
|
usage: usage,
|
||||||
|
discussion: discussion.render(),
|
||||||
|
version: version,
|
||||||
|
shouldDisplay: shouldDisplay,
|
||||||
|
subcommands: subcommands,
|
||||||
|
groupedSubcommands: groupedSubcommands,
|
||||||
|
defaultSubcommand: defaultSubcommand,
|
||||||
|
helpNames: helpNames,
|
||||||
|
aliases: aliases
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Sources/CliDoc/ArgumentParserExtensions/Discussion.swift
Normal file
16
Sources/CliDoc/ArgumentParserExtensions/Discussion.swift
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/// Use the `NodeBuilder` for generating a discussion.
|
||||||
|
public struct Discussion: NodeRepresentable {
|
||||||
|
@usableFromInline
|
||||||
|
let label: any NodeRepresentable
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(@NodeBuilder label: () -> any NodeRepresentable) {
|
||||||
|
self.label = label()
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public func render() -> String {
|
||||||
|
label.render()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
// swiftlint:disable type_name
|
|
||||||
public enum _ConditionalNode<
|
|
||||||
TrueNode: NodeRepresentable,
|
|
||||||
FalseNode: NodeRepresentable
|
|
||||||
>: NodeRepresentable {
|
|
||||||
case first(TrueNode)
|
|
||||||
case second(FalseNode)
|
|
||||||
|
|
||||||
public func render() -> String {
|
|
||||||
switch self {
|
|
||||||
case let .first(node): return node.render()
|
|
||||||
case let .second(node): return node.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct _ManyNode: NodeRepresentable {
|
|
||||||
@usableFromInline
|
|
||||||
let nodes: [any NodeRepresentable]
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let separator: any NodeRepresentable
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init(_ nodes: [any NodeRepresentable], separator: any NodeRepresentable) {
|
|
||||||
self.nodes = nodes
|
|
||||||
self.separator = separator
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func render() -> String {
|
|
||||||
nodes.map { $0.render() }
|
|
||||||
.joined(separator: separator.render())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// swiftlint:enable type_name
|
|
||||||
14
Sources/CliDoc/Modifiers/AnyModifier.swift
Normal file
14
Sources/CliDoc/Modifiers/AnyModifier.swift
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
public struct AnyNodeModifier: NodeModifier {
|
||||||
|
@usableFromInline
|
||||||
|
let modifier: any NodeModifier
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init<N: NodeModifier>(_ modifier: N) {
|
||||||
|
self.modifier = modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
|
||||||
|
modifier.render(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Sources/CliDoc/Modifiers/Color.swift
Normal file
35
Sources/CliDoc/Modifiers/Color.swift
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import Rainbow
|
||||||
|
|
||||||
|
public extension NodeRepresentable {
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func color(_ color: NamedColor) -> any NodeRepresentable {
|
||||||
|
modifier(ColorModifier(color: color))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension NodeModifier {
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
static func color(_ color: NamedColor) -> Self where Self == AnyNodeModifier {
|
||||||
|
.init(ColorModifier(color: color))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
struct ColorModifier: NodeModifier, @unchecked Sendable {
|
||||||
|
@usableFromInline
|
||||||
|
let color: NamedColor
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
init(color: NamedColor) {
|
||||||
|
self.color = color
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
|
||||||
|
node.render().applyingColor(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
48
Sources/CliDoc/Modifiers/LabelStyle.swift
Normal file
48
Sources/CliDoc/Modifiers/LabelStyle.swift
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import Rainbow
|
||||||
|
|
||||||
|
public extension NodeRepresentable {
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func labelStyle(_ style: any NodeModifier) -> any NodeRepresentable {
|
||||||
|
return modifier(LabelStyleModifier(style))
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func labelStyle(_ color: NamedColor) -> any NodeRepresentable {
|
||||||
|
return modifier(LabelStyleModifier(.color(color)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func labelStyle(_ style: Style) -> any NodeRepresentable {
|
||||||
|
return modifier(LabelStyleModifier(.style(style)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func labelStyle(_ styles: Style...) -> any NodeRepresentable {
|
||||||
|
return modifier(LabelStyleModifier(.style(styles)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
struct LabelStyleModifier: NodeModifier {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let modifier: any NodeModifier
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
init(_ modifier: any NodeModifier) {
|
||||||
|
self.modifier = modifier
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
|
||||||
|
if let node = node as? LabeledContent {
|
||||||
|
return LabeledContent(separator: node.separator) {
|
||||||
|
node.label.modifier(modifier)
|
||||||
|
} content: {
|
||||||
|
node.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
}
|
||||||
44
Sources/CliDoc/Modifiers/Repeating.swift
Normal file
44
Sources/CliDoc/Modifiers/Repeating.swift
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
public extension NodeRepresentable {
|
||||||
|
@inlinable
|
||||||
|
func repeating(_ count: Int, separator: (any NodeRepresentable)? = nil) -> any NodeRepresentable {
|
||||||
|
modifier(RepeatingNode(count: count, separator: separator))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
struct RepeatingNode: NodeModifier {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let count: Int
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let separator: (any NodeRepresentable)?
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
init(
|
||||||
|
count: Int,
|
||||||
|
separator: (any NodeRepresentable)?
|
||||||
|
) {
|
||||||
|
self.count = count
|
||||||
|
self.separator = separator
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
|
||||||
|
let input = node.render()
|
||||||
|
var output = input
|
||||||
|
let separator = self.separator != nil
|
||||||
|
? self.separator!.render()
|
||||||
|
: ""
|
||||||
|
|
||||||
|
guard count > 0 else { return output }
|
||||||
|
|
||||||
|
var count = count - 1
|
||||||
|
while count > 0 {
|
||||||
|
output = "\(output)\(separator)\(input)"
|
||||||
|
count -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Sources/CliDoc/Modifiers/Style.swift
Normal file
41
Sources/CliDoc/Modifiers/Style.swift
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import Rainbow
|
||||||
|
|
||||||
|
public extension NodeRepresentable {
|
||||||
|
@inlinable
|
||||||
|
func style(_ styles: Style...) -> any NodeRepresentable {
|
||||||
|
modifier(StyleModifier(styles: styles))
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
func style(_ styles: [Style]) -> any NodeRepresentable {
|
||||||
|
modifier(StyleModifier(styles: styles))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension NodeModifier {
|
||||||
|
@inlinable
|
||||||
|
static func style(_ styles: Style...) -> Self where Self == AnyNodeModifier {
|
||||||
|
.init(StyleModifier(styles: styles))
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
static func style(_ styles: [Style]) -> Self where Self == AnyNodeModifier {
|
||||||
|
.init(StyleModifier(styles: styles))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
struct StyleModifier: NodeModifier, @unchecked Sendable {
|
||||||
|
@usableFromInline
|
||||||
|
let styles: [Style]
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
init(styles: [Style]) {
|
||||||
|
self.styles = styles
|
||||||
|
}
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
|
||||||
|
styles.reduce(node.render()) { $0.applyingStyle($1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,4 +28,75 @@ public enum NodeBuilder {
|
|||||||
.second(component)
|
.second(component)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func buildBlock<N: NodeRepresentable>(_ components: N...) -> _ManyNode {
|
||||||
|
.init(components)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This breaks things ??
|
||||||
|
// public static func buildExpression<N: NodeRepresentable>(_ expression: N) -> AnyNode {
|
||||||
|
// expression.eraseToAnyNode()
|
||||||
|
// }
|
||||||
|
|
||||||
|
public static func buildOptional<N: NodeRepresentable>(_ component: N?) -> AnyNode {
|
||||||
|
component?.eraseToAnyNode() ?? AnyNode { Text.empty() }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func buildFinalResult<N: NodeRepresentable>(_ component: N) -> N {
|
||||||
|
component
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func buildLimitedAvailability<N: NodeRepresentable>(_ component: N) -> N {
|
||||||
|
component
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These are nodes that are only used by the `NodeBuilder` / internally.
|
||||||
|
|
||||||
|
// swiftlint:disable type_name
|
||||||
|
public enum _ConditionalNode<
|
||||||
|
TrueNode: NodeRepresentable,
|
||||||
|
FalseNode: NodeRepresentable
|
||||||
|
>: NodeRepresentable {
|
||||||
|
case first(TrueNode)
|
||||||
|
case second(FalseNode)
|
||||||
|
|
||||||
|
public func render() -> String {
|
||||||
|
switch self {
|
||||||
|
case let .first(node): return node.render()
|
||||||
|
case let .second(node): return node.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct _ManyNode: NodeRepresentable {
|
||||||
|
@usableFromInline
|
||||||
|
let nodes: [any NodeRepresentable]
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let separator: any NodeRepresentable
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
init(
|
||||||
|
_ nodes: [any NodeRepresentable],
|
||||||
|
separator: AnyNode = Text.empty().eraseToAnyNode()
|
||||||
|
) {
|
||||||
|
self.nodes = nodes
|
||||||
|
self.separator = separator
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(_ nodes: [any NodeRepresentable], separator: any NodeRepresentable) {
|
||||||
|
self.nodes = nodes
|
||||||
|
self.separator = separator
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public func render() -> String {
|
||||||
|
nodes.map { $0.render() }
|
||||||
|
.joined(separator: separator.render())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:enable type_name
|
||||||
|
|||||||
9
Sources/CliDoc/NodeModifier.swift
Normal file
9
Sources/CliDoc/NodeModifier.swift
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
public protocol NodeModifier: Sendable {
|
||||||
|
func render(_ node: any NodeRepresentable) -> any NodeRepresentable
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension NodeRepresentable {
|
||||||
|
func modifier(_ modifier: NodeModifier) -> any NodeRepresentable {
|
||||||
|
modifier.render(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
public struct Group: NodeRepresentable {
|
public struct Group: NodeRepresentable {
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
let node: any NodeRepresentable
|
let node: any NodeRepresentable
|
||||||
@@ -25,10 +24,10 @@ public struct Group: NodeRepresentable {
|
|||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public init(
|
public init(
|
||||||
separator: AnyNode = Space().eraseToAnyNode(),
|
separator: String = " ",
|
||||||
@NodeBuilder _ build: () -> any NodeRepresentable
|
@NodeBuilder _ build: () -> any NodeRepresentable
|
||||||
) {
|
) {
|
||||||
self.init(separator: separator, node: build())
|
self.init(separator: separator.eraseToAnyNode(), node: build())
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
|
|||||||
@@ -1,32 +1,45 @@
|
|||||||
@preconcurrency import Rainbow
|
@preconcurrency import Rainbow
|
||||||
|
|
||||||
public struct Header: NodeRepresentable {
|
public struct Header: NodeRepresentable, @unchecked Sendable {
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
let node: Text
|
let node: any NodeRepresentable
|
||||||
|
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
let styles: [Style]
|
let color: NamedColor?
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let styles: [Style]?
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(
|
||||||
|
@NodeBuilder _ build: () -> any NodeRepresentable
|
||||||
|
) {
|
||||||
|
self.node = build()
|
||||||
|
self.color = nil
|
||||||
|
self.styles = nil
|
||||||
|
}
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public init(
|
public init(
|
||||||
_ text: String,
|
_ text: String,
|
||||||
color: NamedColor? = .yellow,
|
color: NamedColor? = .yellow,
|
||||||
styles: [Style] = [.bold]
|
styles: [Style]? = [.bold]
|
||||||
) {
|
) {
|
||||||
var text = Text(text)
|
self.color = color
|
||||||
if let color {
|
self.node = Text(text)
|
||||||
text = text.color(color)
|
|
||||||
}
|
|
||||||
self.node = text
|
|
||||||
self.styles = styles
|
self.styles = styles
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public func render() -> String {
|
public func render() -> String {
|
||||||
styles.reduce(node) { node, style in
|
var node = node
|
||||||
node.style(style)
|
if let color {
|
||||||
|
node = node.color(color)
|
||||||
}
|
}
|
||||||
.render()
|
if let styles {
|
||||||
|
node = node.style(styles)
|
||||||
|
}
|
||||||
|
return node.render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
Sources/CliDoc/Nodes/LabeledContent.swift
Normal file
46
Sources/CliDoc/Nodes/LabeledContent.swift
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
public struct LabeledContent: NodeRepresentable {
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let label: any NodeRepresentable
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let content: any NodeRepresentable
|
||||||
|
|
||||||
|
@usableFromInline
|
||||||
|
let separator: any NodeRepresentable
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public init(
|
||||||
|
separator: (any NodeRepresentable)? = nil,
|
||||||
|
@NodeBuilder label: () -> any NodeRepresentable,
|
||||||
|
@NodeBuilder content: () -> any NodeRepresentable
|
||||||
|
) {
|
||||||
|
self.separator = separator ?? "\n"
|
||||||
|
self.label = label()
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
public func render() -> String {
|
||||||
|
Group(separator: separator) {
|
||||||
|
label
|
||||||
|
content
|
||||||
|
}.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension LabeledContent {
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
init(
|
||||||
|
_ label: String,
|
||||||
|
separator: (any NodeRepresentable)? = nil,
|
||||||
|
@NodeBuilder content: () -> any NodeRepresentable
|
||||||
|
) {
|
||||||
|
self.init(separator: separator) {
|
||||||
|
Text(label)
|
||||||
|
} content: {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
public extension NodeRepresentable {
|
|
||||||
@inlinable
|
|
||||||
func repeating(_ count: Int) -> some NodeRepresentable {
|
|
||||||
RepeatingNode(self, count: count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
struct RepeatingNode: NodeRepresentable {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let node: any NodeRepresentable
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let count: Int
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
init(_ node: any NodeRepresentable, count: Int) {
|
|
||||||
self.node = node
|
|
||||||
self.count = count
|
|
||||||
}
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
func render() -> String {
|
|
||||||
var string = node.render()
|
|
||||||
guard count > 0 else { return string }
|
|
||||||
var count = count - 1
|
|
||||||
while count > 0 {
|
|
||||||
string = "\(string)\(node.render())"
|
|
||||||
count -= 1
|
|
||||||
}
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
// TODO: Remove init(header:...) initializers.
|
|
||||||
public struct Section: NodeRepresentable {
|
|
||||||
@usableFromInline
|
|
||||||
let node: any NodeRepresentable
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init<N: NodeRepresentable>(@NodeBuilder _ build: () -> N) {
|
|
||||||
self.node = build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func render() -> String {
|
|
||||||
node.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init<Content: NodeRepresentable, Separator: NodeRepresentable>(
|
|
||||||
header: String,
|
|
||||||
separator: Separator,
|
|
||||||
@NodeBuilder content: () -> Content
|
|
||||||
) {
|
|
||||||
self.init {
|
|
||||||
Text(header)
|
|
||||||
separator
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init<Content: NodeRepresentable>(
|
|
||||||
header: String,
|
|
||||||
inline: Bool = false,
|
|
||||||
@NodeBuilder content: () -> Content
|
|
||||||
) {
|
|
||||||
self.init(
|
|
||||||
header: header,
|
|
||||||
separator: inline ? Space().eraseToAnyNode() : NewLine().eraseToAnyNode(),
|
|
||||||
content: content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
Sources/CliDoc/Nodes/String+NodeRepresentable.swift
Normal file
6
Sources/CliDoc/Nodes/String+NodeRepresentable.swift
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
extension String: NodeRepresentable {
|
||||||
|
@inlinable
|
||||||
|
public func render() -> String {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,43 +13,6 @@ public struct Text: NodeRepresentable {
|
|||||||
public func render() -> String {
|
public func render() -> String {
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func color(_ color: NamedColor) -> Self {
|
|
||||||
.init(text.applyingColor(color))
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func style(_ style: Style) -> Self {
|
|
||||||
.init(text.applyingStyle(style))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct NewLine: NodeRepresentable {
|
|
||||||
@usableFromInline
|
|
||||||
let node: any NodeRepresentable
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init(count: Int = 1) {
|
|
||||||
self.node = Text("\n").repeating(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func render() -> String {
|
|
||||||
node.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Space: NodeRepresentable {
|
|
||||||
let node: any NodeRepresentable
|
|
||||||
|
|
||||||
public init(count: Int = 1) {
|
|
||||||
self.node = Text(" ").repeating(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func render() -> String {
|
|
||||||
node.render()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension NodeRepresentable {
|
public extension NodeRepresentable {
|
||||||
@@ -58,12 +21,12 @@ public extension NodeRepresentable {
|
|||||||
Text("")
|
Text("")
|
||||||
}
|
}
|
||||||
|
|
||||||
static func newLine(count: Int = 1) -> Self where Self == NewLine {
|
static func newLine(count: Int = 1) -> Self where Self == AnyNode {
|
||||||
NewLine(count: count)
|
"\n".repeating(count).eraseToAnyNode()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func space(count: Int = 1) -> Self where Self == Space {
|
static func space(count: Int = 1) -> Self where Self == AnyNode {
|
||||||
Space(count: count)
|
" ".repeating(count).eraseToAnyNode()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +1,99 @@
|
|||||||
@_spi(Internal) import CliDoc
|
@_spi(Internal) import CliDoc
|
||||||
|
@preconcurrency import Rainbow
|
||||||
import Testing
|
import Testing
|
||||||
|
import XCTest
|
||||||
|
|
||||||
@Test
|
final class CliDocTests: XCTestCase {
|
||||||
func testNodeBuilder() {
|
|
||||||
let node = AnyNode {
|
override func setUp() {
|
||||||
Text("foo").color(.green).style(.bold)
|
super.setUp()
|
||||||
NewLine()
|
Rainbow.outputTarget = .console
|
||||||
Text("bar").repeating(2)
|
Rainbow.enabled = true
|
||||||
}
|
}
|
||||||
let expected = """
|
|
||||||
\("foo".green.bold)
|
|
||||||
barbar
|
|
||||||
"""
|
|
||||||
#expect(node.render() == expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(
|
func testStringChecks() {
|
||||||
arguments: [
|
let expected = "Foo".green.bold
|
||||||
(true, "foo bar"),
|
let notexpected = "Foo"
|
||||||
(false, """
|
|
||||||
foo
|
XCTAssert("Foo".green.bold == expected)
|
||||||
bar
|
XCTAssert("Foo".green.bold != notexpected)
|
||||||
""")
|
}
|
||||||
]
|
|
||||||
)
|
func testRepeatingModifier() {
|
||||||
func testSection(
|
let node = AnyNode {
|
||||||
inline: Bool,
|
Text("foo").color(.green).style(.bold)
|
||||||
expected: String
|
"\n".repeating(2)
|
||||||
) {
|
Text("bar").repeating(2, separator: " ")
|
||||||
let node = AnyNode {
|
}
|
||||||
Section(
|
let expected = """
|
||||||
header: "foo",
|
\("foo".green.bold)
|
||||||
separator: inline ? Space().eraseToAnyNode() : NewLine().eraseToAnyNode()
|
|
||||||
) {
|
bar bar
|
||||||
Text("bar")
|
"""
|
||||||
|
XCTAssert(node.render() == expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGroup1() {
|
||||||
|
let arguments = [
|
||||||
|
(true, "foo bar"),
|
||||||
|
(false, """
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
""")
|
||||||
|
]
|
||||||
|
|
||||||
|
for (inline, expected) in arguments {
|
||||||
|
let node = AnyNode {
|
||||||
|
Group(separator: inline ? " " : "\n") {
|
||||||
|
Text("foo")
|
||||||
|
Text("bar")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
XCTAssert(node.render() == expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
print(node.render())
|
func testHeader() {
|
||||||
#expect(node.render() == expected)
|
let header = Header("Foo")
|
||||||
}
|
let expected = "\("Foo".yellow.bold)"
|
||||||
|
XCTAssert(header.render() == expected)
|
||||||
|
|
||||||
@Test
|
let header2 = Header {
|
||||||
func testHeader() {
|
"Foo".yellow.bold
|
||||||
let header = Header("Foo")
|
}
|
||||||
let expected = "\("Foo".yellow.bold)"
|
XCTAssert(header2.render() == expected)
|
||||||
#expect(header.render() == expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func testGroup() {
|
|
||||||
let group = Group {
|
|
||||||
Text("foo")
|
|
||||||
Text("bar")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print(group.render())
|
func testGroup() {
|
||||||
#expect(group.render() == "foo bar")
|
let group = Group {
|
||||||
|
Text("foo")
|
||||||
|
Text("bar")
|
||||||
|
}
|
||||||
|
|
||||||
let group2 = Group(separator: .newLine()) {
|
XCTAssert(group.render() == "foo bar")
|
||||||
Text("foo")
|
|
||||||
Text("bar")
|
let group2 = Group(separator: "\n") {
|
||||||
|
Text("foo")
|
||||||
|
Text("bar")
|
||||||
|
}
|
||||||
|
let expected = """
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
"""
|
||||||
|
XCTAssert(group2.render() == expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLabeledContent() {
|
||||||
|
let node = LabeledContent("Foo") {
|
||||||
|
Text("Bar")
|
||||||
|
}
|
||||||
|
.labelStyle(.green)
|
||||||
|
.labelStyle(.bold)
|
||||||
|
|
||||||
|
let expected = """
|
||||||
|
\("Foo".green.bold)
|
||||||
|
Bar
|
||||||
|
"""
|
||||||
|
XCTAssert(node.render() == expected)
|
||||||
}
|
}
|
||||||
let expected = """
|
|
||||||
foo
|
|
||||||
bar
|
|
||||||
"""
|
|
||||||
#expect(group2.render() == expected)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user