diff --git a/Package.resolved b/Package.resolved index ffe2e48..1c04b30 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "8767e1814bf3d2b706110688b4b4d253de070cc5be3f0f91d5790acb7d0b7ad5", + "originHash" : "def70abefdc3133b863ecf24e1b413af00133f95cabea13d0ff42d7283910a58", "pins" : [ { "identity" : "combine-schedulers", @@ -28,6 +28,15 @@ "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", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index f00b788..73d6723 100644 --- a/Package.swift +++ b/Package.swift @@ -8,21 +8,21 @@ let package = Package( platforms: [.macOS(.v14)], products: [ .executable(name: "hpa", targets: ["hpa"]), - .library(name: "CliClient", targets: ["CliClient"]), - .library(name: "CliDoc", targets: ["CliDoc"]) + .library(name: "CliClient", targets: ["CliClient"]) ], dependencies: [ .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/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: [ .executableTarget( name: "hpa", dependencies: [ "CliClient", - "CliDoc", .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "CliDoc", package: "swift-cli-doc"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "ShellClient", package: "swift-shell-client") ] @@ -35,14 +35,6 @@ let package = Package( .product(name: "ShellClient", package: "swift-shell-client") ] ), - .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"]) + .testTarget(name: "CliClientTests", dependencies: ["CliClient"]) ] ) diff --git a/Sources/CliDoc/ArgumentParserExtensions/Abstract.swift b/Sources/CliDoc/ArgumentParserExtensions/Abstract.swift deleted file mode 100644 index 3549021..0000000 --- a/Sources/CliDoc/ArgumentParserExtensions/Abstract.swift +++ /dev/null @@ -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() - } - -} diff --git a/Sources/CliDoc/ArgumentParserExtensions/CommandConfiguration+NodeBuilder.swift b/Sources/CliDoc/ArgumentParserExtensions/CommandConfiguration+NodeBuilder.swift deleted file mode 100644 index acf10e9..0000000 --- a/Sources/CliDoc/ArgumentParserExtensions/CommandConfiguration+NodeBuilder.swift +++ /dev/null @@ -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 - ) - } -} diff --git a/Sources/CliDoc/ArgumentParserExtensions/Discussion.swift b/Sources/CliDoc/ArgumentParserExtensions/Discussion.swift deleted file mode 100644 index 0717bde..0000000 --- a/Sources/CliDoc/ArgumentParserExtensions/Discussion.swift +++ /dev/null @@ -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() - } - -} diff --git a/Sources/CliDoc/Modifiers/AnyModifier.swift b/Sources/CliDoc/Modifiers/AnyModifier.swift deleted file mode 100644 index 8780264..0000000 --- a/Sources/CliDoc/Modifiers/AnyModifier.swift +++ /dev/null @@ -1,14 +0,0 @@ -public struct AnyNodeModifier: NodeModifier { - @usableFromInline - let modifier: any NodeModifier - - @inlinable - public init(_ modifier: N) { - self.modifier = modifier - } - - @inlinable - public func render(_ node: any NodeRepresentable) -> any NodeRepresentable { - modifier.render(node) - } -} diff --git a/Sources/CliDoc/Modifiers/Color.swift b/Sources/CliDoc/Modifiers/Color.swift deleted file mode 100644 index 8cca8e9..0000000 --- a/Sources/CliDoc/Modifiers/Color.swift +++ /dev/null @@ -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) - } -} diff --git a/Sources/CliDoc/Modifiers/LabelStyle.swift b/Sources/CliDoc/Modifiers/LabelStyle.swift deleted file mode 100644 index e85fe1b..0000000 --- a/Sources/CliDoc/Modifiers/LabelStyle.swift +++ /dev/null @@ -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 - } -} diff --git a/Sources/CliDoc/Modifiers/LabeledContentStyle.swift b/Sources/CliDoc/Modifiers/LabeledContentStyle.swift deleted file mode 100644 index fc0e9ce..0000000 --- a/Sources/CliDoc/Modifiers/LabeledContentStyle.swift +++ /dev/null @@ -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 } - } - } -} diff --git a/Sources/CliDoc/Modifiers/Repeating.swift b/Sources/CliDoc/Modifiers/Repeating.swift deleted file mode 100644 index dfb39f7..0000000 --- a/Sources/CliDoc/Modifiers/Repeating.swift +++ /dev/null @@ -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 - } -} diff --git a/Sources/CliDoc/Modifiers/Style.swift b/Sources/CliDoc/Modifiers/Style.swift deleted file mode 100644 index 922e8a2..0000000 --- a/Sources/CliDoc/Modifiers/Style.swift +++ /dev/null @@ -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) } - } -} diff --git a/Sources/CliDoc/Node.swift b/Sources/CliDoc/Node.swift deleted file mode 100644 index b2a69fd..0000000 --- a/Sources/CliDoc/Node.swift +++ /dev/null @@ -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" } - } -} diff --git a/Sources/CliDoc/NodeBuilder.swift b/Sources/CliDoc/NodeBuilder.swift deleted file mode 100644 index 5a46ae7..0000000 --- a/Sources/CliDoc/NodeBuilder.swift +++ /dev/null @@ -1,110 +0,0 @@ -@resultBuilder -public enum NodeBuilder { - - public static func buildPartialBlock(first: N) -> N { - first - } - - public static func buildPartialBlock( - accumulated: N0, - next: N1 - ) -> _ManyNode { - .init([accumulated, next], separator: .empty()) - } - - public static func buildArray(_ components: [N]) -> _ManyNode { - .init(components, separator: .empty()) - } - - public static func buildEither( - first component: N - ) -> N { - component - } - - public static func buildEither( - second component: N - ) -> N { - component - } - - public static func buildBlock(_ components: N...) -> _ManyNode { - .init(components) - } - - // This breaks things ?? -// public static func buildExpression(_ expression: N) -> AnyNode { -// expression.eraseToAnyNode() -// } - - public static func buildOptional(_ component: N?) -> _ConditionalNode { - switch component { - case let .some(node): - return .first(node) - case .none: - return .second(.empty()) - } - } - - public static func buildFinalResult(_ component: N) -> N { - component - } - - public static func buildLimitedAvailability(_ 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 diff --git a/Sources/CliDoc/NodeModifier.swift b/Sources/CliDoc/NodeModifier.swift deleted file mode 100644 index 788ca25..0000000 --- a/Sources/CliDoc/NodeModifier.swift +++ /dev/null @@ -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) - } -} diff --git a/Sources/CliDoc/NodeRepresentable.swift b/Sources/CliDoc/NodeRepresentable.swift deleted file mode 100644 index 47923d8..0000000 --- a/Sources/CliDoc/NodeRepresentable.swift +++ /dev/null @@ -1,3 +0,0 @@ -public protocol NodeRepresentable: Sendable { - func render() -> String -} diff --git a/Sources/CliDoc/Nodes/AnyNode.swift b/Sources/CliDoc/Nodes/AnyNode.swift deleted file mode 100644 index f20132f..0000000 --- a/Sources/CliDoc/Nodes/AnyNode.swift +++ /dev/null @@ -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(@NodeBuilder _ build: () -> N) { - self.makeBody = build().render - } - - @inlinable - public init(_ 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) - } -} diff --git a/Sources/CliDoc/Nodes/Group.swift b/Sources/CliDoc/Nodes/Group.swift deleted file mode 100644 index a15aeaa..0000000 --- a/Sources/CliDoc/Nodes/Group.swift +++ /dev/null @@ -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: 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() - } -} diff --git a/Sources/CliDoc/Nodes/Header.swift b/Sources/CliDoc/Nodes/Header.swift deleted file mode 100644 index b60937e..0000000 --- a/Sources/CliDoc/Nodes/Header.swift +++ /dev/null @@ -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() - } -} diff --git a/Sources/CliDoc/Nodes/LabeledContent.swift b/Sources/CliDoc/Nodes/LabeledContent.swift deleted file mode 100644 index 289abb2..0000000 --- a/Sources/CliDoc/Nodes/LabeledContent.swift +++ /dev/null @@ -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: 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 - } - } -} diff --git a/Sources/CliDoc/Nodes/Section.swift b/Sources/CliDoc/Nodes/Section.swift deleted file mode 100644 index b761ca5..0000000 --- a/Sources/CliDoc/Nodes/Section.swift +++ /dev/null @@ -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 - } -} diff --git a/Sources/CliDoc/Nodes/ShellCommand.swift b/Sources/CliDoc/Nodes/ShellCommand.swift deleted file mode 100644 index 317acd9..0000000 --- a/Sources/CliDoc/Nodes/ShellCommand.swift +++ /dev/null @@ -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() - } -} diff --git a/Sources/CliDoc/Nodes/String+NodeRepresentable.swift b/Sources/CliDoc/Nodes/String+NodeRepresentable.swift deleted file mode 100644 index 139d280..0000000 --- a/Sources/CliDoc/Nodes/String+NodeRepresentable.swift +++ /dev/null @@ -1,13 +0,0 @@ -extension String: NodeRepresentable { - @inlinable - public func render() -> String { - self - } -} - -extension String: Node { - - public var body: some NodeRepresentable { - self - } -} diff --git a/Sources/CliDoc/Nodes/Text.swift b/Sources/CliDoc/Nodes/Text.swift deleted file mode 100644 index 8ea863c..0000000 --- a/Sources/CliDoc/Nodes/Text.swift +++ /dev/null @@ -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() - } - -} diff --git a/Sources/hpa/CreateCommand.swift b/Sources/hpa/CreateCommand.swift index ac9015a..638b984 100644 --- a/Sources/hpa/CreateCommand.swift +++ b/Sources/hpa/CreateCommand.swift @@ -8,7 +8,7 @@ struct CreateCommand: AsyncParsableCommand { static let commandName = "create" - static let configuration = CommandConfiguration.playbook2( + static let configuration = CommandConfiguration.playbook( commandName: commandName, abstract: "Create a home performance assesment project.", examples: ( diff --git a/Sources/hpa/Internal/CommandConfigurationExtensions.swift b/Sources/hpa/Internal/CommandConfigurationExtensions.swift index 88545e9..c22e0b9 100644 --- a/Sources/hpa/Internal/CommandConfigurationExtensions.swift +++ b/Sources/hpa/Internal/CommandConfigurationExtensions.swift @@ -4,66 +4,17 @@ import Rainbow 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( commandName: String, abstract: String, parentCommand: String? = nil, - examples: (label: String, example: String)... + examples: Example... ) -> Self { - .create( + .init( commandName: commandName, - abstract: abstract, - discussion: .default( - usesExtraArgs: true, - parentCommand: parentCommand, - examples: examples, - seeAlso: .seeAlso(label: "Ansible playbook options.", command: "ansible-playbook") - ) + abstract: Abstract { abstract.blue }, + usage: Usage.default(commandName: commandName, parentCommand: parentCommand), + discussion: Discussion.playbook(examples: examples) ) } } @@ -72,272 +23,27 @@ func createAbstract(_ string: String) -> String { "\(string.blue)" } -// TODO: Remove -struct Discussion { - let nodes: [Node] +extension Usage where Content == AnyTextNode { - static func `default`( - usesExtraArgs: Bool, - parentCommand: String?, - examples: [(label: String, example: String)], - seeAlso: Node? - ) -> Self { - var nodes = Array.defaultNodes + examples.nodes(parentCommand) - if let seeAlso { nodes.append(seeAlso) } - if usesExtraArgs && examples.count > 0 { nodes.append(.important(label: Constants.importantExtraArgsNote)) } - 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() + static func `default`(commandName: String, parentCommand: String?) -> Self { + .init { + HStack { + HStack { + "\(Constants.appName)" + if let parentCommand { + parentCommand + } + commandName } + .color(.blue).bold() - // Get the last element of the example container, which will be the - // text for the example. - guard let exampleNode = exampleNodes.last else { - print(exampleNodes) - 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) - ) + "[OPTIONS]".color(.green) + "[ARGUMENTS]".color(.cyan) + "--" + "[EXTRA-OPTIONS]".color(.magenta) } - } - self.nodes = nodes - } - func render() -> String { nodes.render() } -} - -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() + .eraseToAnyTextNode() } } } - -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 - } -} diff --git a/Sources/hpa/Internal/Discussion+playbook.swift b/Sources/hpa/Internal/Discussion+playbook.swift index c8baf10..9b1c69a 100644 --- a/Sources/hpa/Internal/Discussion+playbook.swift +++ b/Sources/hpa/Internal/Discussion+playbook.swift @@ -1,86 +1,127 @@ import CliDoc -extension CliDoc.Discussion { +extension CliDoc.Discussion where Content == AnyTextNode { static func playbook( - examples: [(label: String, example: String)] + examples: [Example] ) -> Self { .init { - Section { + VStack { Note.mostOptionsNotRequired - Examples(examples: examples) + ExampleSection.default(examples: examples) SeeAlso(label: "Ansible playbook options.", command: "ansible-playbook --help") 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 { - .init(command: "\(Constants.appName) \(command)") + .init { "\(Constants.appName) \(command)" } symbol: { "$" } } } -struct SeeAlso: NodeRepresentable { +struct SeeAlso: TextNode { - let node: any NodeRepresentable - - init(label: String, command: String) { - 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 + let title = "SEE ALSO:" + let label: Label + let content: Content init( - _ label: String = "NOTE:", - @NodeBuilder _ content: () -> any NodeRepresentable + @TextBuilder content: () -> Content, + @TextBuilder label: () -> Label ) { - self.node = LabeledContent( - separator: " ", - label: { label.yellow.bold }, - content: { content().style(.italic) } - ) + self.content = content() + self.label = label() } - func render() -> String { - node.render() + var body: some TextNode { + 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: 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 { .init { "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: NodeRepresentable { +struct ImportantNote: TextNode { - let node: any NodeRepresentable + let label: String = "IMPORTANT NOTE:" + let content: Content init( - _ label: String = "IMPORTANT NOTE:", - @NodeBuilder _ content: () -> any NodeRepresentable + @TextBuilder _ content: () -> Content ) { - self.node = LabeledContent( - separator: "\n", - label: { label.red.bold.underline }, - content: { content().style(.italic) } - ) + self.content = content() } - func render() -> String { - node.render() + var body: some TextNode { + HStack { + label.color(.red).bold().underline() + content + } } +} + +extension ImportantNote where Content == String { static var passingExtraArgs: Self { .init { """ diff --git a/Sources/hpa/VaultCommands/VaultCommand.swift b/Sources/hpa/VaultCommands/VaultCommand.swift index 380e6a5..febe2bf 100644 --- a/Sources/hpa/VaultCommands/VaultCommand.swift +++ b/Sources/hpa/VaultCommands/VaultCommand.swift @@ -1,4 +1,5 @@ import ArgumentParser +import CliDoc struct VaultCommand: AsyncParsableCommand { @@ -7,12 +8,21 @@ struct VaultCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: commandName, abstract: createAbstract("Vault commands."), - discussion: Discussion( - .text(""" - Allows you to run `ansible-vault` commands on your project or project-template. - """), - .seeAlso(label: "Ansible Vault", command: "ansible-vault") - ).render(), + discussion: Discussion { + VStack { + """ + Allows you to run `ansible-vault` commands on your project or project-template. + + Ansible-vault allows you to store sensitive variables in an encrypted format. + """ + SeeAlso { + "ansible-vault --help" + } label: { + "Ansible Vault" + } + } + .separator(.newLine(count: 2)) + }, subcommands: [ EncryptCommand.self, DecryptCommand.self ]