feat: Working on node builder

This commit is contained in:
2024-12-01 15:25:42 -05:00
parent ff49b12198
commit 96a1fac07b
11 changed files with 357 additions and 21 deletions

View File

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

View 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
View 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" }
}
}

View File

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

View 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
}
}

View 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()
}
}

View File

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

View File

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

View 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.
"""
}
}
}