feat: Updates cli documentation to use the swift-cli-doc package.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "8767e1814bf3d2b706110688b4b4d253de070cc5be3f0f91d5790acb7d0b7ad5",
|
"originHash" : "def70abefdc3133b863ecf24e1b413af00133f95cabea13d0ff42d7283910a58",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "combine-schedulers",
|
"identity" : "combine-schedulers",
|
||||||
@@ -28,6 +28,15 @@
|
|||||||
"version" : "1.5.0"
|
"version" : "1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-cli-doc",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://git.housh.dev/michael/swift-cli-doc.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "e524056dc65c5ce7a6a77bdea4e5fa0bf724019b",
|
||||||
|
"version" : "0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-clocks",
|
"identity" : "swift-clocks",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ let package = Package(
|
|||||||
platforms: [.macOS(.v14)],
|
platforms: [.macOS(.v14)],
|
||||||
products: [
|
products: [
|
||||||
.executable(name: "hpa", targets: ["hpa"]),
|
.executable(name: "hpa", targets: ["hpa"]),
|
||||||
.library(name: "CliClient", targets: ["CliClient"]),
|
.library(name: "CliClient", targets: ["CliClient"])
|
||||||
.library(name: "CliDoc", targets: ["CliDoc"])
|
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
|
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
|
||||||
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.2"),
|
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.2"),
|
||||||
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.1.0")
|
.package(url: "https://github.com/m-housh/swift-shell-client.git", from: "0.1.0"),
|
||||||
|
.package(url: "https://git.housh.dev/michael/swift-cli-doc.git", from: "0.2.0")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "hpa",
|
name: "hpa",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"CliClient",
|
"CliClient",
|
||||||
"CliDoc",
|
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||||
|
.product(name: "CliDoc", package: "swift-cli-doc"),
|
||||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||||
.product(name: "ShellClient", package: "swift-shell-client")
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
]
|
]
|
||||||
@@ -35,14 +35,6 @@ let package = Package(
|
|||||||
.product(name: "ShellClient", package: "swift-shell-client")
|
.product(name: "ShellClient", package: "swift-shell-client")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
.testTarget(name: "CliClientTests", dependencies: ["CliClient"]),
|
.testTarget(name: "CliClientTests", dependencies: ["CliClient"])
|
||||||
.target(
|
|
||||||
name: "CliDoc",
|
|
||||||
dependencies: [
|
|
||||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
|
||||||
.product(name: "ShellClient", package: "swift-shell-client")
|
|
||||||
]
|
|
||||||
),
|
|
||||||
.testTarget(name: "CliDocTests", dependencies: ["CliDoc"])
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
/// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import ArgumentParser
|
|
||||||
|
|
||||||
public extension CommandConfiguration {
|
|
||||||
|
|
||||||
/// Use `NodeBuilder` syntax for generating the abstract and discussion parameters.
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
/// 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,14 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
public enum LabelContentStyle: Sendable {
|
|
||||||
case inline
|
|
||||||
case `default`
|
|
||||||
case custom(any NodeRepresentable)
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension NodeRepresentable {
|
|
||||||
@inlinable
|
|
||||||
func labeledContentStyle(_ style: LabelContentStyle) -> any NodeRepresentable {
|
|
||||||
modifier(LabeledContentStyleModifier(style: style))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
struct LabeledContentStyleModifier: NodeModifier {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let style: LabelContentStyle
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
init(style: LabelContentStyle) {
|
|
||||||
self.style = style
|
|
||||||
}
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
|
|
||||||
if let many = node as? _ManyNode {
|
|
||||||
var nodes = many.nodes
|
|
||||||
print("nodes: \(nodes)")
|
|
||||||
for (idx, node) in nodes.enumerated() {
|
|
||||||
if let labeled = node as? LabeledContent {
|
|
||||||
let newNode = labeled.applyingContentStyle(style)
|
|
||||||
nodes[idx] = newNode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return _ManyNode(nodes, separator: many.separator)
|
|
||||||
} else if let labeled = node as? LabeledContent {
|
|
||||||
return labeled.applyingContentStyle(style)
|
|
||||||
}
|
|
||||||
print("no many or labeled nodes found in: \(node)")
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension LabeledContent {
|
|
||||||
|
|
||||||
func applyingContentStyle(_ style: LabelContentStyle) -> Self {
|
|
||||||
switch style {
|
|
||||||
case .inline:
|
|
||||||
return .init(separator: " ") { label } content: { content }
|
|
||||||
case .default:
|
|
||||||
return .init(separator: "\n") { label } content: { content }
|
|
||||||
case let .custom(custom):
|
|
||||||
return .init(separator: custom) { label } content: { content }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// swiftlint:disable type_name
|
|
||||||
public protocol Node: NodeRepresentable {
|
|
||||||
associatedtype _Body: NodeRepresentable
|
|
||||||
typealias Body = _Body
|
|
||||||
|
|
||||||
@NodeBuilder
|
|
||||||
var body: Body { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
// swiftlint:enable type_name
|
|
||||||
|
|
||||||
public extension Node {
|
|
||||||
@inlinable
|
|
||||||
func render() -> String {
|
|
||||||
body.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Foo: Node {
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public var body: some NodeRepresentable {
|
|
||||||
LabeledContent("Foo") { "Bar" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
@resultBuilder
|
|
||||||
public enum NodeBuilder {
|
|
||||||
|
|
||||||
public static func buildPartialBlock<N: NodeRepresentable>(first: N) -> N {
|
|
||||||
first
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func buildPartialBlock<N0: NodeRepresentable, N1: NodeRepresentable>(
|
|
||||||
accumulated: N0,
|
|
||||||
next: N1
|
|
||||||
) -> _ManyNode {
|
|
||||||
.init([accumulated, next], separator: .empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func buildArray<N: NodeRepresentable>(_ components: [N]) -> _ManyNode {
|
|
||||||
.init(components, separator: .empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func buildEither<N: NodeRepresentable>(
|
|
||||||
first component: N
|
|
||||||
) -> N {
|
|
||||||
component
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func buildEither<N: NodeRepresentable>(
|
|
||||||
second component: N
|
|
||||||
) -> N {
|
|
||||||
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?) -> _ConditionalNode<N, Text> {
|
|
||||||
switch component {
|
|
||||||
case let .some(node):
|
|
||||||
return .first(node)
|
|
||||||
case .none:
|
|
||||||
return .second(.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: any NodeRepresentable = "\n" as any NodeRepresentable
|
|
||||||
) {
|
|
||||||
// Flatten the nodes.
|
|
||||||
var allNodes = [any NodeRepresentable]()
|
|
||||||
for node in nodes {
|
|
||||||
if let many = node as? Self {
|
|
||||||
allNodes += many.nodes
|
|
||||||
} else {
|
|
||||||
allNodes.append(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.nodes = allNodes
|
|
||||||
self.separator = separator
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func render() -> String {
|
|
||||||
nodes.map { $0.render() }
|
|
||||||
.joined(separator: separator.render())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// swiftlint:enable type_name
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
public protocol NodeModifier: Sendable {
|
|
||||||
func render(_ node: any NodeRepresentable) -> any NodeRepresentable
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension NodeRepresentable {
|
|
||||||
func modifier(_ modifier: NodeModifier) -> any NodeRepresentable {
|
|
||||||
modifier.render(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
public protocol NodeRepresentable: Sendable {
|
|
||||||
func render() -> String
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// TODO: This doesn't seem correct, maybe remove this type.
|
|
||||||
public struct AnyNode: NodeRepresentable {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let makeBody: @Sendable () -> String
|
|
||||||
|
|
||||||
// @usableFromInline
|
|
||||||
// let node: Value
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init<N: NodeRepresentable>(@NodeBuilder _ build: () -> N) {
|
|
||||||
self.makeBody = build().render
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init<N: NodeRepresentable>(_ node: N) {
|
|
||||||
self.makeBody = node.render
|
|
||||||
}
|
|
||||||
|
|
||||||
public func render() -> String {
|
|
||||||
makeBody()
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// public var body: some NodeRepresentable {
|
|
||||||
// node
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @inlinable
|
|
||||||
// public func render() -> String {
|
|
||||||
// node.render()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension NodeRepresentable {
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
func eraseToAnyNode() -> AnyNode {
|
|
||||||
.init(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
public struct Group: NodeRepresentable {
|
|
||||||
@usableFromInline
|
|
||||||
let node: any NodeRepresentable
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init(
|
|
||||||
separator: any NodeRepresentable = " ",
|
|
||||||
@NodeBuilder node: () -> any NodeRepresentable
|
|
||||||
) {
|
|
||||||
if let many = node as? _ManyNode {
|
|
||||||
self.node = _ManyNode(many.nodes, separator: separator)
|
|
||||||
} else {
|
|
||||||
self.node = node()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @inlinable
|
|
||||||
// public init(
|
|
||||||
// separator: any NodeRepresentable,
|
|
||||||
// @NodeBuilder _ build: () -> any NodeRepresentable
|
|
||||||
// ) {
|
|
||||||
// self.init(separator: separator.eraseToAnyNode(), node: build())
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @inlinable
|
|
||||||
// public init(
|
|
||||||
// separator: String = " ",
|
|
||||||
// @NodeBuilder _ build: () -> any NodeRepresentable
|
|
||||||
// ) {
|
|
||||||
// self.init(separator: separator.eraseToAnyNode(), node: build())
|
|
||||||
// }
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func render() -> String {
|
|
||||||
node.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct Group2<Content: NodeRepresentable>: Node {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let content: Content
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let separator: any NodeRepresentable
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init(
|
|
||||||
separator: any NodeRepresentable = " ",
|
|
||||||
@NodeBuilder _ content: () -> Content
|
|
||||||
) {
|
|
||||||
self.content = content()
|
|
||||||
self.separator = separator
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public var body: some NodeRepresentable {
|
|
||||||
guard let content = content as? _ManyNode else {
|
|
||||||
return content.eraseToAnyNode()
|
|
||||||
}
|
|
||||||
return _ManyNode(content.nodes, separator: separator).eraseToAnyNode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
@preconcurrency import Rainbow
|
|
||||||
|
|
||||||
public struct Header: NodeRepresentable, @unchecked Sendable {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let node: any NodeRepresentable
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let color: NamedColor?
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let styles: [Style]?
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init(
|
|
||||||
@NodeBuilder _ build: () -> any NodeRepresentable
|
|
||||||
) {
|
|
||||||
self.node = build()
|
|
||||||
self.color = nil
|
|
||||||
self.styles = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init(
|
|
||||||
_ text: String,
|
|
||||||
color: NamedColor? = .yellow,
|
|
||||||
styles: [Style]? = [.bold]
|
|
||||||
) {
|
|
||||||
self.color = color
|
|
||||||
self.node = Text(text)
|
|
||||||
self.styles = styles
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func render() -> String {
|
|
||||||
var node = node
|
|
||||||
if let color {
|
|
||||||
node = node.color(color)
|
|
||||||
}
|
|
||||||
if let styles {
|
|
||||||
node = node.style(styles)
|
|
||||||
}
|
|
||||||
return node.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public struct LabeledContent2<Label: Node, Content: Node>: Node {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let separator: any NodeRepresentable
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let label: Label
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let content: Content
|
|
||||||
|
|
||||||
public init(
|
|
||||||
separator: any NodeRepresentable = " ",
|
|
||||||
@NodeBuilder label: () -> Label,
|
|
||||||
@NodeBuilder content: () -> Content
|
|
||||||
) {
|
|
||||||
self.separator = separator
|
|
||||||
self.label = label()
|
|
||||||
self.content = content()
|
|
||||||
}
|
|
||||||
|
|
||||||
public var body: some NodeRepresentable {
|
|
||||||
Group2(separator: separator) {
|
|
||||||
label
|
|
||||||
content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
public struct Section: NodeRepresentable {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let content: any NodeRepresentable
|
|
||||||
|
|
||||||
public init(@NodeBuilder content: () -> any NodeRepresentable) {
|
|
||||||
self.content = content()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func render() -> String {
|
|
||||||
guard let many = content as? _ManyNode else {
|
|
||||||
return content.render() + "\n\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
let node = _ManyNode(many.nodes, separator: "\n\n")
|
|
||||||
return node.render()
|
|
||||||
|
|
||||||
// var output = ""
|
|
||||||
// let lastIndex = many.nodes.count - 1
|
|
||||||
//
|
|
||||||
// for (idx, content) in many.nodes.enumerated() {
|
|
||||||
// if idx != lastIndex {
|
|
||||||
// output += content.render() + "\n\n"
|
|
||||||
// } else {
|
|
||||||
// output += content.render() + "\n"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
public struct ShellCommand: NodeRepresentable {
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let symbol: any NodeRepresentable
|
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
let command: any NodeRepresentable
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init(
|
|
||||||
@NodeBuilder symbol: () -> any NodeRepresentable,
|
|
||||||
@NodeBuilder command: () -> any NodeRepresentable
|
|
||||||
) {
|
|
||||||
self.symbol = symbol()
|
|
||||||
self.command = command()
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init(
|
|
||||||
symbol: String = " $",
|
|
||||||
@NodeBuilder command: () -> any NodeRepresentable
|
|
||||||
) {
|
|
||||||
self.init { symbol } command: { command() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init(
|
|
||||||
symbol: String = " $",
|
|
||||||
command: String
|
|
||||||
) {
|
|
||||||
self.init { symbol } command: { command }
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public func render() -> String {
|
|
||||||
Group(separator: " ") {
|
|
||||||
symbol
|
|
||||||
command
|
|
||||||
}.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
extension String: NodeRepresentable {
|
|
||||||
@inlinable
|
|
||||||
public func render() -> String {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension String: Node {
|
|
||||||
|
|
||||||
public var body: some NodeRepresentable {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
@preconcurrency import Rainbow
|
|
||||||
|
|
||||||
public struct Text: Node {
|
|
||||||
@usableFromInline
|
|
||||||
let text: String
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init(_ text: String) {
|
|
||||||
self.text = text
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public var body: some NodeRepresentable {
|
|
||||||
text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension NodeRepresentable {
|
|
||||||
|
|
||||||
static func empty() -> Self where Self == Text {
|
|
||||||
Text("")
|
|
||||||
}
|
|
||||||
|
|
||||||
static func newLine(count: Int = 1) -> Self where Self == AnyNode {
|
|
||||||
"\n".repeating(count).eraseToAnyNode()
|
|
||||||
}
|
|
||||||
|
|
||||||
static func space(count: Int = 1) -> Self where Self == AnyNode {
|
|
||||||
" ".repeating(count).eraseToAnyNode()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@ struct CreateCommand: AsyncParsableCommand {
|
|||||||
|
|
||||||
static let commandName = "create"
|
static let commandName = "create"
|
||||||
|
|
||||||
static let configuration = CommandConfiguration.playbook2(
|
static let configuration = CommandConfiguration.playbook(
|
||||||
commandName: commandName,
|
commandName: commandName,
|
||||||
abstract: "Create a home performance assesment project.",
|
abstract: "Create a home performance assesment project.",
|
||||||
examples: (
|
examples: (
|
||||||
|
|||||||
@@ -4,66 +4,17 @@ import Rainbow
|
|||||||
|
|
||||||
extension CommandConfiguration {
|
extension CommandConfiguration {
|
||||||
|
|
||||||
static func create(
|
|
||||||
commandName: String,
|
|
||||||
abstract: String,
|
|
||||||
usesExtraArgs: Bool = true,
|
|
||||||
discussion: Discussion
|
|
||||||
) -> Self {
|
|
||||||
.init(
|
|
||||||
commandName: commandName,
|
|
||||||
abstract: createAbstract(abstract),
|
|
||||||
discussion: discussion.render()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func create(
|
|
||||||
commandName: String,
|
|
||||||
abstract: String,
|
|
||||||
usesExtraArgs: Bool = true,
|
|
||||||
discussion nodes: [Node]
|
|
||||||
) -> Self {
|
|
||||||
.init(
|
|
||||||
commandName: commandName,
|
|
||||||
abstract: createAbstract(abstract),
|
|
||||||
discussion: Discussion(nodes: nodes, usesExtraArgs: usesExtraArgs).render()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func playbook2(
|
|
||||||
commandName: String,
|
|
||||||
abstract: String,
|
|
||||||
parentCommand: String? = nil,
|
|
||||||
examples: (label: String, example: String)...
|
|
||||||
) -> Self {
|
|
||||||
.init(
|
|
||||||
commandName: commandName,
|
|
||||||
abstract: Abstract { abstract.blue },
|
|
||||||
usage: """
|
|
||||||
\(Constants.appName)\(parentCommand != nil ? " \(parentCommand!)" : "") \(commandName)
|
|
||||||
""".blue.bold.italic
|
|
||||||
+ " [OPTIONS]".green
|
|
||||||
+ " [ARGUMENTS]".cyan
|
|
||||||
+ " --" + " [EXTRA-OPTIONS...]".magenta,
|
|
||||||
discussion: CliDoc.Discussion.playbook(examples: examples)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func playbook(
|
static func playbook(
|
||||||
commandName: String,
|
commandName: String,
|
||||||
abstract: String,
|
abstract: String,
|
||||||
parentCommand: String? = nil,
|
parentCommand: String? = nil,
|
||||||
examples: (label: String, example: String)...
|
examples: Example...
|
||||||
) -> Self {
|
) -> Self {
|
||||||
.create(
|
.init(
|
||||||
commandName: commandName,
|
commandName: commandName,
|
||||||
abstract: abstract,
|
abstract: Abstract { abstract.blue },
|
||||||
discussion: .default(
|
usage: Usage.default(commandName: commandName, parentCommand: parentCommand),
|
||||||
usesExtraArgs: true,
|
discussion: Discussion.playbook(examples: examples)
|
||||||
parentCommand: parentCommand,
|
|
||||||
examples: examples,
|
|
||||||
seeAlso: .seeAlso(label: "Ansible playbook options.", command: "ansible-playbook")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,272 +23,27 @@ func createAbstract(_ string: String) -> String {
|
|||||||
"\(string.blue)"
|
"\(string.blue)"
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove
|
extension Usage where Content == AnyTextNode {
|
||||||
struct Discussion {
|
|
||||||
let nodes: [Node]
|
|
||||||
|
|
||||||
static func `default`(
|
static func `default`(commandName: String, parentCommand: String?) -> Self {
|
||||||
usesExtraArgs: Bool,
|
.init {
|
||||||
parentCommand: String?,
|
HStack {
|
||||||
examples: [(label: String, example: String)],
|
HStack {
|
||||||
seeAlso: Node?
|
"\(Constants.appName)"
|
||||||
) -> Self {
|
if let parentCommand {
|
||||||
var nodes = Array.defaultNodes + examples.nodes(parentCommand)
|
parentCommand
|
||||||
if let seeAlso { nodes.append(seeAlso) }
|
}
|
||||||
if usesExtraArgs && examples.count > 0 { nodes.append(.important(label: Constants.importantExtraArgsNote)) }
|
commandName
|
||||||
return .init(
|
|
||||||
nodes: nodes,
|
|
||||||
usesExtraArgs: usesExtraArgs
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(usesExtraArgs: Bool = true, _ nodes: Node...) {
|
|
||||||
self.init(nodes: nodes, usesExtraArgs: usesExtraArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(nodes: [Node], usesExtraArgs: Bool = true) {
|
|
||||||
var nodes = nodes
|
|
||||||
|
|
||||||
if let firstExampleIndex = nodes.firstIndex(where: \.isExampleNode) {
|
|
||||||
nodes.insert(.exampleHeading, at: firstExampleIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
if usesExtraArgs {
|
|
||||||
if let lastExampleIndex = nodes.lastIndex(where: \.isExampleNode) {
|
|
||||||
guard case let .example(.container(exampleNodes, _)) = nodes[lastExampleIndex] else {
|
|
||||||
// this should never happen.
|
|
||||||
print(nodes[lastExampleIndex])
|
|
||||||
fatalError()
|
|
||||||
}
|
}
|
||||||
|
.color(.blue).bold()
|
||||||
|
|
||||||
// Get the last element of the example container, which will be the
|
"[OPTIONS]".color(.green)
|
||||||
// text for the example.
|
"[ARGUMENTS]".color(.cyan)
|
||||||
guard let exampleNode = exampleNodes.last else {
|
"--"
|
||||||
print(exampleNodes)
|
"[EXTRA-OPTIONS]".color(.magenta)
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace the first element, which is the header so that we can
|
|
||||||
// replace it with a new header.
|
|
||||||
nodes.insert(
|
|
||||||
.example(.exampleContainer(
|
|
||||||
"Passing extra args.",
|
|
||||||
"\(exampleNode.render().replacingOccurrences(of: " $ ", with: ""))"
|
|
||||||
+ " -- --vault-id \"myId@$SCRIPTS/vault-gopass-client\""
|
|
||||||
)),
|
|
||||||
at: nodes.index(after: lastExampleIndex)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
self.nodes = nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
func render() -> String { nodes.render() }
|
.eraseToAnyTextNode()
|
||||||
}
|
|
||||||
|
|
||||||
enum Node {
|
|
||||||
|
|
||||||
struct Separator {
|
|
||||||
|
|
||||||
let string: String
|
|
||||||
let count: Int
|
|
||||||
|
|
||||||
init(_ string: String, repeating count: Int = 1) {
|
|
||||||
self.string = string
|
|
||||||
self.count = count
|
|
||||||
}
|
|
||||||
|
|
||||||
var value: String { string.repeating(count) }
|
|
||||||
|
|
||||||
static var empty: Self { .init("") }
|
|
||||||
|
|
||||||
static func space(_ count: Int = 1) -> Self {
|
|
||||||
.init(" ", repeating: count)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func newLine(_ count: Int = 1) -> Self {
|
|
||||||
.init("\n", repeating: count)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func tab(_ count: Int) -> Self {
|
|
||||||
.init("\t", repeating: count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A container that holds onto other nodes to be rendered.
|
|
||||||
indirect case container(_ nodes: [Node], separator: Separator = .newLine())
|
|
||||||
|
|
||||||
/// A container to identify example nodes, so some other nodes can get injected
|
|
||||||
/// when this is rendered.
|
|
||||||
///
|
|
||||||
/// NOTE: Things blow up if this is not used correctly, at least when using the `Discussion`
|
|
||||||
/// to render the nodes, so this is not safe to create aside from the static methods that
|
|
||||||
/// create an example node. If not using in the context of a `Discussion` then it's fine to
|
|
||||||
/// use, as it doesn't do anything except stand as an type identifier.
|
|
||||||
indirect case example(_ node: Node)
|
|
||||||
|
|
||||||
/// A root node that renders the given string without modification.
|
|
||||||
case text(_ text: String)
|
|
||||||
|
|
||||||
static func container(separator: Separator = .newLine(), _ nodes: Node...) -> Self {
|
|
||||||
.container(nodes, separator: separator)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func container(_ lhs: Node, _ rhs: Node, separator: Separator = .newLine()) -> Self {
|
|
||||||
.container([lhs, rhs], separator: separator)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func styledText(_ text: String, color: NamedColor? = nil, styles: [Style]? = nil) -> Self {
|
|
||||||
var string = text
|
|
||||||
if let color {
|
|
||||||
string = string.applyingColor(color)
|
|
||||||
}
|
|
||||||
if let styles {
|
|
||||||
for style in styles {
|
|
||||||
string = string.applyingStyle(style)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return .text(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func styledText(_ text: String, color: NamedColor? = nil, style: Style) -> Self {
|
|
||||||
.styledText(text, color: color, styles: [style])
|
|
||||||
}
|
|
||||||
|
|
||||||
static func boldYellowText(_ text: String) -> Self {
|
|
||||||
.styledText(text, color: .yellow, styles: [.bold])
|
|
||||||
}
|
|
||||||
|
|
||||||
static func section(header: Node, content: Node, separator: Separator = .newLine()) -> Self {
|
|
||||||
.container([header, content], separator: separator)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func section(header: String, label: Node, separator: Separator = .newLine()) -> Self {
|
|
||||||
.section(header: .boldYellowText(header), content: label, separator: separator)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func important(label: String) -> Self {
|
|
||||||
.section(
|
|
||||||
header: .text("IMPORTANT NOTE:".red.bold.underline),
|
|
||||||
content: .text(label.italic),
|
|
||||||
separator: .newLine()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func note(_ text: String, inline: Bool = true) -> Self {
|
|
||||||
.section(header: "NOTE:", label: .text(text.italic), separator: inline ? .space() : .newLine())
|
|
||||||
}
|
|
||||||
|
|
||||||
static func shellCommand(_ text: String, symbol: String = " $") -> Self {
|
|
||||||
.text("\(symbol) \(text)")
|
|
||||||
}
|
|
||||||
|
|
||||||
static func seeAlso(label: String, command: String, appendHelpToCommand: Bool = true) -> Self {
|
|
||||||
.container([
|
|
||||||
.container(.boldYellowText("See Also:"), .text(label.italic), separator: .space()),
|
|
||||||
.shellCommand("\(appendHelpToCommand ? command + " --help" : command)")
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
static var exampleHeading: Self {
|
|
||||||
.section(header: "Examples:", label: .text("Some common examples."), separator: .space())
|
|
||||||
}
|
|
||||||
|
|
||||||
var isExampleNode: Bool {
|
|
||||||
if case .example = self { return true }
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate static func exampleContainer(_ label: String, _ command: String) -> Self {
|
|
||||||
.container(
|
|
||||||
.exampleLabel("\(label)\n"),
|
|
||||||
.shellCommand(command, symbol: " $"),
|
|
||||||
separator: .empty
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Renders an example usage of the command.
|
|
||||||
static func example(label: String, example: String, parentCommand: String?) -> Self {
|
|
||||||
let string: String
|
|
||||||
if let parent = parentCommand {
|
|
||||||
string = "\(Constants.appName) \(parent) \(example)"
|
|
||||||
} else {
|
|
||||||
string = "\(Constants.appName) \(example)"
|
|
||||||
}
|
|
||||||
return .example(.exampleContainer(label, string))
|
|
||||||
}
|
|
||||||
|
|
||||||
static func exampleLabel(_ label: String) -> Node {
|
|
||||||
.text(" \(label.green.italic)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func render() -> String {
|
|
||||||
switch self {
|
|
||||||
case let .container(nodes, separator):
|
|
||||||
return nodes.render(separator: separator)
|
|
||||||
case let .text(text):
|
|
||||||
return text
|
|
||||||
case let .example(node):
|
|
||||||
return node.render()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Node: ExpressibleByStringLiteral {
|
|
||||||
|
|
||||||
typealias StringLiteralType = String
|
|
||||||
|
|
||||||
init(stringLiteral value: String) {
|
|
||||||
self = .text(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Node: ExpressibleByArrayLiteral {
|
|
||||||
|
|
||||||
typealias ArrayLiteralElement = Node
|
|
||||||
|
|
||||||
init(arrayLiteral elements: Node...) {
|
|
||||||
self = .container(elements)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Array where Element == (label: String, example: String) {
|
|
||||||
func nodes(_ parentCommand: String?) -> [Node] {
|
|
||||||
map { .example(label: $0.label, example: $0.example, parentCommand: parentCommand) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Array where Element == Node {
|
|
||||||
|
|
||||||
static var defaultNodes: Self {
|
|
||||||
[.note("Most options are not required if you have a configuration file setup.")]
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func render(separator: Node.Separator = .newLine()) -> String {
|
|
||||||
map {
|
|
||||||
// Strip of any new-line characters from the last section of the rendered string
|
|
||||||
// of the node. This allows us to have a consistent single new-line between each
|
|
||||||
// rendered node.
|
|
||||||
var string = $0.render()[...]
|
|
||||||
while string.hasSuffix("\n") {
|
|
||||||
string = string.dropLast()
|
|
||||||
}
|
|
||||||
return String(string)
|
|
||||||
}
|
|
||||||
.joined(separator: separator.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension String {
|
|
||||||
func repeating(_ count: Int) -> Self {
|
|
||||||
guard count > 0 else { return self }
|
|
||||||
var count = count
|
|
||||||
var output = self
|
|
||||||
|
|
||||||
while count > 0 {
|
|
||||||
output = "\(output)\(self)"
|
|
||||||
count -= 1
|
|
||||||
}
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,86 +1,127 @@
|
|||||||
import CliDoc
|
import CliDoc
|
||||||
|
|
||||||
extension CliDoc.Discussion {
|
extension CliDoc.Discussion where Content == AnyTextNode {
|
||||||
static func playbook(
|
static func playbook(
|
||||||
examples: [(label: String, example: String)]
|
examples: [Example]
|
||||||
) -> Self {
|
) -> Self {
|
||||||
.init {
|
.init {
|
||||||
Section {
|
VStack {
|
||||||
Note.mostOptionsNotRequired
|
Note.mostOptionsNotRequired
|
||||||
Examples(examples: examples)
|
ExampleSection.default(examples: examples)
|
||||||
SeeAlso(label: "Ansible playbook options.", command: "ansible-playbook --help")
|
SeeAlso(label: "Ansible playbook options.", command: "ansible-playbook --help")
|
||||||
ImportantNote.passingExtraArgs
|
ImportantNote.passingExtraArgs
|
||||||
}
|
}
|
||||||
// .labeledContentStyle(.custom("foo:"))
|
.separator(.newLine(count: 2))
|
||||||
|
.eraseToAnyTextNode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ShellCommand {
|
extension Array where Element == Example {
|
||||||
|
func addingPassingOptions(command: String) -> Self {
|
||||||
|
var output = self
|
||||||
|
output.append((
|
||||||
|
label: "Passing extra arguments / options",
|
||||||
|
example: "\(command) -- --vaultId=myId@$SCRIPTS/vault-gopass-client"
|
||||||
|
))
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ExampleSection where Header == String, Label == String {
|
||||||
|
static func `default`(examples: [Example], usesPassingExtraOptions: Bool = true) -> some TextNode {
|
||||||
|
var examples = examples
|
||||||
|
if usesPassingExtraOptions, let first = examples.first {
|
||||||
|
examples = examples.addingPassingOptions(command: first.example)
|
||||||
|
}
|
||||||
|
return Self(examples: examples) {
|
||||||
|
"EXAMPLES:"
|
||||||
|
} label: {
|
||||||
|
"Some common usage examples."
|
||||||
|
}
|
||||||
|
.exampleStyle(HPAExampleStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HPAExampleStyle: ExampleStyle {
|
||||||
|
|
||||||
|
func render(content: ExampleConfiguration) -> some TextNode {
|
||||||
|
VStack {
|
||||||
|
content.examples.map { example in
|
||||||
|
VStack {
|
||||||
|
example.label.color(.green).bold()
|
||||||
|
ShellCommand.hpaCommand(example.example)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.separator(.newLine(count: 2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ShellCommand where Content == String, Symbol == String {
|
||||||
static func hpaCommand(_ command: String) -> Self {
|
static func hpaCommand(_ command: String) -> Self {
|
||||||
.init(command: "\(Constants.appName) \(command)")
|
.init { "\(Constants.appName) \(command)" } symbol: { "$" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SeeAlso: NodeRepresentable {
|
struct SeeAlso<Label: TextNode, Content: TextNode>: TextNode {
|
||||||
|
|
||||||
let node: any NodeRepresentable
|
let title = "SEE ALSO:"
|
||||||
|
let label: Label
|
||||||
init(label: String, command: String) {
|
let content: Content
|
||||||
self.node = Group(separator: "\n") {
|
|
||||||
Note("SEE ALSO:") {
|
|
||||||
label
|
|
||||||
}
|
|
||||||
ShellCommand(command: command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func render() -> String {
|
|
||||||
node.render()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Examples: NodeRepresentable {
|
|
||||||
typealias Example = (label: String, example: String)
|
|
||||||
let examples: [Example]
|
|
||||||
|
|
||||||
func render() -> String {
|
|
||||||
Group(separator: "\n") {
|
|
||||||
Note("EXAMPLES:") { "Common usage examples." }
|
|
||||||
for (label, command) in examples {
|
|
||||||
LabeledContent("\t\(label.green.bold)", separator: "\n") {
|
|
||||||
ShellCommand.hpaCommand(command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let first = examples.first {
|
|
||||||
LabeledContent("\n\tPassing extra options.".green.bold, separator: "\n") {
|
|
||||||
ShellCommand.hpaCommand("\(first.example) -- --vault-id \"myId@$SCRIPTS/vault-gopass-client\"")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.render()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Note: NodeRepresentable {
|
|
||||||
|
|
||||||
let node: any NodeRepresentable
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
_ label: String = "NOTE:",
|
@TextBuilder content: () -> Content,
|
||||||
@NodeBuilder _ content: () -> any NodeRepresentable
|
@TextBuilder label: () -> Label
|
||||||
) {
|
) {
|
||||||
self.node = LabeledContent(
|
self.content = content()
|
||||||
separator: " ",
|
self.label = label()
|
||||||
label: { label.yellow.bold },
|
|
||||||
content: { content().style(.italic) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func render() -> String {
|
var body: some TextNode {
|
||||||
node.render()
|
VStack {
|
||||||
|
LabeledContent {
|
||||||
|
label.italic()
|
||||||
|
} label: {
|
||||||
|
title.color(.yellow).bold().underline()
|
||||||
|
}
|
||||||
|
ShellCommand { content } symbol: { "$" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SeeAlso where Content == String, Label == Empty {
|
||||||
|
init(command: String) {
|
||||||
|
self.init { "\(Constants.appName) \(command)" } label: { Empty() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SeeAlso where Content == String, Label == String {
|
||||||
|
init(label: String, command: String) {
|
||||||
|
self.init { "\(Constants.appName) \(command)" } label: { label }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Note<Content: TextNode>: TextNode {
|
||||||
|
|
||||||
|
let label: String = "NOTE:"
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(
|
||||||
|
@TextBuilder _ content: () -> Content
|
||||||
|
) {
|
||||||
|
self.content = content()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var body: some TextNode {
|
||||||
|
HStack {
|
||||||
|
label.color(.yellow).bold()
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Note where Content == String {
|
||||||
static var mostOptionsNotRequired: Self {
|
static var mostOptionsNotRequired: Self {
|
||||||
.init {
|
.init {
|
||||||
"Most options are not required if you have a configuration file setup."
|
"Most options are not required if you have a configuration file setup."
|
||||||
@@ -88,26 +129,27 @@ struct Note: NodeRepresentable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fix the text.
|
struct ImportantNote<Content: TextNode>: TextNode {
|
||||||
struct ImportantNote: NodeRepresentable {
|
|
||||||
|
|
||||||
let node: any NodeRepresentable
|
let label: String = "IMPORTANT NOTE:"
|
||||||
|
let content: Content
|
||||||
|
|
||||||
init(
|
init(
|
||||||
_ label: String = "IMPORTANT NOTE:",
|
@TextBuilder _ content: () -> Content
|
||||||
@NodeBuilder _ content: () -> any NodeRepresentable
|
|
||||||
) {
|
) {
|
||||||
self.node = LabeledContent(
|
self.content = content()
|
||||||
separator: "\n",
|
|
||||||
label: { label.red.bold.underline },
|
|
||||||
content: { content().style(.italic) }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func render() -> String {
|
var body: some TextNode {
|
||||||
node.render()
|
HStack {
|
||||||
|
label.color(.red).bold().underline()
|
||||||
|
content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ImportantNote where Content == String {
|
||||||
static var passingExtraArgs: Self {
|
static var passingExtraArgs: Self {
|
||||||
.init {
|
.init {
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import CliDoc
|
||||||
|
|
||||||
struct VaultCommand: AsyncParsableCommand {
|
struct VaultCommand: AsyncParsableCommand {
|
||||||
|
|
||||||
@@ -7,12 +8,21 @@ struct VaultCommand: AsyncParsableCommand {
|
|||||||
static let configuration = CommandConfiguration(
|
static let configuration = CommandConfiguration(
|
||||||
commandName: commandName,
|
commandName: commandName,
|
||||||
abstract: createAbstract("Vault commands."),
|
abstract: createAbstract("Vault commands."),
|
||||||
discussion: Discussion(
|
discussion: Discussion {
|
||||||
.text("""
|
VStack {
|
||||||
Allows you to run `ansible-vault` commands on your project or project-template.
|
"""
|
||||||
"""),
|
Allows you to run `ansible-vault` commands on your project or project-template.
|
||||||
.seeAlso(label: "Ansible Vault", command: "ansible-vault")
|
|
||||||
).render(),
|
Ansible-vault allows you to store sensitive variables in an encrypted format.
|
||||||
|
"""
|
||||||
|
SeeAlso {
|
||||||
|
"ansible-vault --help"
|
||||||
|
} label: {
|
||||||
|
"Ansible Vault"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.separator(.newLine(count: 2))
|
||||||
|
},
|
||||||
subcommands: [
|
subcommands: [
|
||||||
EncryptCommand.self, DecryptCommand.self
|
EncryptCommand.self, DecryptCommand.self
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user