feat: Working on node builder
This commit is contained in:
@@ -2,6 +2,7 @@ import ArgumentParser
|
||||
|
||||
public extension CommandConfiguration {
|
||||
|
||||
/// Use `NodeBuilder` syntax for generating the abstract and discussion parameters.
|
||||
init(
|
||||
commandName: String? = nil,
|
||||
abstract: Abstract,
|
||||
|
||||
58
Sources/CliDoc/Modifiers/LabeledContentStyle.swift
Normal file
58
Sources/CliDoc/Modifiers/LabeledContentStyle.swift
Normal file
@@ -0,0 +1,58 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Sources/CliDoc/Node.swift
Normal file
26
Sources/CliDoc/Node.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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" }
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,16 @@ public enum NodeBuilder {
|
||||
.init(components, separator: .empty())
|
||||
}
|
||||
|
||||
public static func buildEither<N0: NodeRepresentable, N1: NodeRepresentable>(
|
||||
first component: N0
|
||||
) -> _ConditionalNode<N0, N1> {
|
||||
.first(component)
|
||||
public static func buildEither<N: NodeRepresentable>(
|
||||
first component: N
|
||||
) -> N {
|
||||
component
|
||||
}
|
||||
|
||||
public static func buildEither<N0: NodeRepresentable, N1: NodeRepresentable>(
|
||||
second component: N1
|
||||
) -> _ConditionalNode<N0, N1> {
|
||||
.second(component)
|
||||
public static func buildEither<N: NodeRepresentable>(
|
||||
second component: N
|
||||
) -> N {
|
||||
component
|
||||
}
|
||||
|
||||
public static func buildBlock<N: NodeRepresentable>(_ components: N...) -> _ManyNode {
|
||||
@@ -37,8 +37,13 @@ public enum NodeBuilder {
|
||||
// expression.eraseToAnyNode()
|
||||
// }
|
||||
|
||||
public static func buildOptional<N: NodeRepresentable>(_ component: N?) -> AnyNode {
|
||||
component?.eraseToAnyNode() ?? AnyNode { Text.empty() }
|
||||
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 {
|
||||
@@ -79,15 +84,18 @@ public struct _ManyNode: NodeRepresentable {
|
||||
@usableFromInline
|
||||
init(
|
||||
_ nodes: [any NodeRepresentable],
|
||||
separator: AnyNode = Text.empty().eraseToAnyNode()
|
||||
separator: any NodeRepresentable = "\n" as any NodeRepresentable
|
||||
) {
|
||||
self.nodes = nodes
|
||||
self.separator = separator
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public init(_ nodes: [any NodeRepresentable], separator: any NodeRepresentable) {
|
||||
self.nodes = nodes
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
30
Sources/CliDoc/Nodes/Section.swift
Normal file
30
Sources/CliDoc/Nodes/Section.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
41
Sources/CliDoc/Nodes/ShellCommand.swift
Normal file
41
Sources/CliDoc/Nodes/ShellCommand.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ struct CreateCommand: AsyncParsableCommand {
|
||||
|
||||
static let commandName = "create"
|
||||
|
||||
static let configuration = CommandConfiguration.playbook(
|
||||
static let configuration = CommandConfiguration.playbook2(
|
||||
commandName: commandName,
|
||||
abstract: "Create a home performance assesment project.",
|
||||
examples: (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ArgumentParser
|
||||
import CliDoc
|
||||
import Rainbow
|
||||
|
||||
extension CommandConfiguration {
|
||||
@@ -29,6 +30,25 @@ extension CommandConfiguration {
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -52,6 +72,7 @@ func createAbstract(_ string: String) -> String {
|
||||
"\(string.blue)"
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
struct Discussion {
|
||||
let nodes: [Node]
|
||||
|
||||
|
||||
120
Sources/hpa/Internal/Discussion+playbook.swift
Normal file
120
Sources/hpa/Internal/Discussion+playbook.swift
Normal file
@@ -0,0 +1,120 @@
|
||||
import CliDoc
|
||||
|
||||
extension CliDoc.Discussion {
|
||||
static func playbook(
|
||||
examples: [(label: String, example: String)]
|
||||
) -> Self {
|
||||
.init {
|
||||
Section {
|
||||
Note.mostOptionsNotRequired
|
||||
Examples(examples: examples)
|
||||
SeeAlso(label: "Ansible playbook options.", command: "ansible-playbook --help")
|
||||
ImportantNote.passingExtraArgs
|
||||
}
|
||||
.labeledContentStyle(.custom("foo:"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ShellCommand {
|
||||
static func hpaCommand(_ command: String) -> Self {
|
||||
.init(command: "\(Constants.appName) \(command)")
|
||||
}
|
||||
}
|
||||
|
||||
struct SeeAlso: NodeRepresentable {
|
||||
|
||||
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
|
||||
|
||||
init(
|
||||
_ label: String = "NOTE:",
|
||||
@NodeBuilder _ content: () -> any NodeRepresentable
|
||||
) {
|
||||
self.node = LabeledContent(
|
||||
separator: " ",
|
||||
label: { label.yellow.bold },
|
||||
content: { content().style(.italic) }
|
||||
)
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
node.render()
|
||||
}
|
||||
|
||||
static var mostOptionsNotRequired: Self {
|
||||
.init {
|
||||
"Most options are not required if you have a configuration file setup."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Fix the text.
|
||||
struct ImportantNote: NodeRepresentable {
|
||||
|
||||
let node: any NodeRepresentable
|
||||
|
||||
init(
|
||||
_ label: String = "IMPORTANT NOTE:",
|
||||
@NodeBuilder _ content: () -> any NodeRepresentable
|
||||
) {
|
||||
self.node = LabeledContent(
|
||||
separator: "\n",
|
||||
label: { label.red.bold.underline },
|
||||
content: { content().style(.italic) }
|
||||
)
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
node.render()
|
||||
}
|
||||
|
||||
static var passingExtraArgs: Self {
|
||||
.init {
|
||||
"""
|
||||
Any extra options passed to the underlying command need to come after
|
||||
`--` otherwise an "Unknown option" error will occur. See above example of passing
|
||||
extra options.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user