feat: Working on node builder

This commit is contained in:
2024-12-01 11:45:05 -05:00
parent 55d8888961
commit ff49b12198
20 changed files with 495 additions and 227 deletions

View File

@@ -38,6 +38,7 @@ let package = Package(
.target(
name: "CliDoc",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "ShellClient", package: "swift-shell-client")
]
),

View File

@@ -0,0 +1,16 @@
/// Use the `NodeBuilder` for generating an abstract.
public struct Abstract: NodeRepresentable {
@usableFromInline
let label: any NodeRepresentable
@inlinable
public init(@NodeBuilder label: () -> any NodeRepresentable) {
self.label = label()
}
@inlinable
public func render() -> String {
label.render()
}
}

View File

@@ -0,0 +1,32 @@
import ArgumentParser
public extension CommandConfiguration {
init(
commandName: String? = nil,
abstract: Abstract,
usage: String? = nil,
discussion: Discussion,
version: String = "",
shouldDisplay: Bool = true,
subcommands: [any ParsableCommand.Type] = [],
groupedSubcommands: [CommandGroup] = [],
defaultSubcommand: ParsableCommand.Type? = nil,
helpNames: NameSpecification? = nil,
aliases: [String] = []
) {
self.init(
commandName: commandName,
abstract: abstract.render(),
usage: usage,
discussion: discussion.render(),
version: version,
shouldDisplay: shouldDisplay,
subcommands: subcommands,
groupedSubcommands: groupedSubcommands,
defaultSubcommand: defaultSubcommand,
helpNames: helpNames,
aliases: aliases
)
}
}

View File

@@ -0,0 +1,16 @@
/// Use the `NodeBuilder` for generating a discussion.
public struct Discussion: NodeRepresentable {
@usableFromInline
let label: any NodeRepresentable
@inlinable
public init(@NodeBuilder label: () -> any NodeRepresentable) {
self.label = label()
}
@inlinable
public func render() -> String {
label.render()
}
}

View File

@@ -1,40 +0,0 @@
import Foundation
// swiftlint:disable type_name
public enum _ConditionalNode<
TrueNode: NodeRepresentable,
FalseNode: NodeRepresentable
>: NodeRepresentable {
case first(TrueNode)
case second(FalseNode)
public func render() -> String {
switch self {
case let .first(node): return node.render()
case let .second(node): return node.render()
}
}
}
public struct _ManyNode: NodeRepresentable {
@usableFromInline
let nodes: [any NodeRepresentable]
@usableFromInline
let separator: any NodeRepresentable
@inlinable
public init(_ nodes: [any NodeRepresentable], separator: any NodeRepresentable) {
self.nodes = nodes
self.separator = separator
}
@inlinable
public func render() -> String {
nodes.map { $0.render() }
.joined(separator: separator.render())
}
}
// swiftlint:enable type_name

View File

@@ -0,0 +1,14 @@
public struct AnyNodeModifier: NodeModifier {
@usableFromInline
let modifier: any NodeModifier
@inlinable
public init<N: NodeModifier>(_ modifier: N) {
self.modifier = modifier
}
@inlinable
public func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
modifier.render(node)
}
}

View File

@@ -0,0 +1,35 @@
import Rainbow
public extension NodeRepresentable {
@inlinable
func color(_ color: NamedColor) -> any NodeRepresentable {
modifier(ColorModifier(color: color))
}
}
public extension NodeModifier {
@inlinable
static func color(_ color: NamedColor) -> Self where Self == AnyNodeModifier {
.init(ColorModifier(color: color))
}
}
@usableFromInline
struct ColorModifier: NodeModifier, @unchecked Sendable {
@usableFromInline
let color: NamedColor
@usableFromInline
init(color: NamedColor) {
self.color = color
}
@usableFromInline
func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
node.render().applyingColor(color)
}
}

View File

@@ -0,0 +1,48 @@
import Rainbow
public extension NodeRepresentable {
@inlinable
func labelStyle(_ style: any NodeModifier) -> any NodeRepresentable {
return modifier(LabelStyleModifier(style))
}
@inlinable
func labelStyle(_ color: NamedColor) -> any NodeRepresentable {
return modifier(LabelStyleModifier(.color(color)))
}
@inlinable
func labelStyle(_ style: Style) -> any NodeRepresentable {
return modifier(LabelStyleModifier(.style(style)))
}
@inlinable
func labelStyle(_ styles: Style...) -> any NodeRepresentable {
return modifier(LabelStyleModifier(.style(styles)))
}
}
@usableFromInline
struct LabelStyleModifier: NodeModifier {
@usableFromInline
let modifier: any NodeModifier
@usableFromInline
init(_ modifier: any NodeModifier) {
self.modifier = modifier
}
@usableFromInline
func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
if let node = node as? LabeledContent {
return LabeledContent(separator: node.separator) {
node.label.modifier(modifier)
} content: {
node.content
}
}
return node
}
}

View File

@@ -0,0 +1,44 @@
public extension NodeRepresentable {
@inlinable
func repeating(_ count: Int, separator: (any NodeRepresentable)? = nil) -> any NodeRepresentable {
modifier(RepeatingNode(count: count, separator: separator))
}
}
@usableFromInline
struct RepeatingNode: NodeModifier {
@usableFromInline
let count: Int
@usableFromInline
let separator: (any NodeRepresentable)?
@usableFromInline
init(
count: Int,
separator: (any NodeRepresentable)?
) {
self.count = count
self.separator = separator
}
@usableFromInline
func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
let input = node.render()
var output = input
let separator = self.separator != nil
? self.separator!.render()
: ""
guard count > 0 else { return output }
var count = count - 1
while count > 0 {
output = "\(output)\(separator)\(input)"
count -= 1
}
return output
}
}

View File

@@ -0,0 +1,41 @@
import Rainbow
public extension NodeRepresentable {
@inlinable
func style(_ styles: Style...) -> any NodeRepresentable {
modifier(StyleModifier(styles: styles))
}
@inlinable
func style(_ styles: [Style]) -> any NodeRepresentable {
modifier(StyleModifier(styles: styles))
}
}
public extension NodeModifier {
@inlinable
static func style(_ styles: Style...) -> Self where Self == AnyNodeModifier {
.init(StyleModifier(styles: styles))
}
@inlinable
static func style(_ styles: [Style]) -> Self where Self == AnyNodeModifier {
.init(StyleModifier(styles: styles))
}
}
@usableFromInline
struct StyleModifier: NodeModifier, @unchecked Sendable {
@usableFromInline
let styles: [Style]
@usableFromInline
init(styles: [Style]) {
self.styles = styles
}
@usableFromInline
func render(_ node: any NodeRepresentable) -> any NodeRepresentable {
styles.reduce(node.render()) { $0.applyingStyle($1) }
}
}

View File

@@ -28,4 +28,75 @@ public enum NodeBuilder {
.second(component)
}
public static func buildBlock<N: NodeRepresentable>(_ components: N...) -> _ManyNode {
.init(components)
}
// This breaks things ??
// public static func buildExpression<N: NodeRepresentable>(_ expression: N) -> AnyNode {
// expression.eraseToAnyNode()
// }
public static func buildOptional<N: NodeRepresentable>(_ component: N?) -> AnyNode {
component?.eraseToAnyNode() ?? AnyNode { Text.empty() }
}
public static func buildFinalResult<N: NodeRepresentable>(_ component: N) -> N {
component
}
public static func buildLimitedAvailability<N: NodeRepresentable>(_ component: N) -> N {
component
}
}
// These are nodes that are only used by the `NodeBuilder` / internally.
// swiftlint:disable type_name
public enum _ConditionalNode<
TrueNode: NodeRepresentable,
FalseNode: NodeRepresentable
>: NodeRepresentable {
case first(TrueNode)
case second(FalseNode)
public func render() -> String {
switch self {
case let .first(node): return node.render()
case let .second(node): return node.render()
}
}
}
public struct _ManyNode: NodeRepresentable {
@usableFromInline
let nodes: [any NodeRepresentable]
@usableFromInline
let separator: any NodeRepresentable
@usableFromInline
init(
_ nodes: [any NodeRepresentable],
separator: AnyNode = Text.empty().eraseToAnyNode()
) {
self.nodes = nodes
self.separator = separator
}
@inlinable
public init(_ nodes: [any NodeRepresentable], separator: any NodeRepresentable) {
self.nodes = nodes
self.separator = separator
}
@inlinable
public func render() -> String {
nodes.map { $0.render() }
.joined(separator: separator.render())
}
}
// swiftlint:enable type_name

View File

@@ -0,0 +1,9 @@
public protocol NodeModifier: Sendable {
func render(_ node: any NodeRepresentable) -> any NodeRepresentable
}
public extension NodeRepresentable {
func modifier(_ modifier: NodeModifier) -> any NodeRepresentable {
modifier.render(self)
}
}

View File

@@ -1,4 +1,3 @@
public struct Group: NodeRepresentable {
@usableFromInline
let node: any NodeRepresentable
@@ -25,10 +24,10 @@ public struct Group: NodeRepresentable {
@inlinable
public init(
separator: AnyNode = Space().eraseToAnyNode(),
separator: String = " ",
@NodeBuilder _ build: () -> any NodeRepresentable
) {
self.init(separator: separator, node: build())
self.init(separator: separator.eraseToAnyNode(), node: build())
}
@inlinable

View File

@@ -1,32 +1,45 @@
@preconcurrency import Rainbow
public struct Header: NodeRepresentable {
public struct Header: NodeRepresentable, @unchecked Sendable {
@usableFromInline
let node: Text
let node: any NodeRepresentable
@usableFromInline
let styles: [Style]
let color: NamedColor?
@usableFromInline
let styles: [Style]?
@inlinable
public init(
@NodeBuilder _ build: () -> any NodeRepresentable
) {
self.node = build()
self.color = nil
self.styles = nil
}
@inlinable
public init(
_ text: String,
color: NamedColor? = .yellow,
styles: [Style] = [.bold]
styles: [Style]? = [.bold]
) {
var text = Text(text)
if let color {
text = text.color(color)
}
self.node = text
self.color = color
self.node = Text(text)
self.styles = styles
}
@inlinable
public func render() -> String {
styles.reduce(node) { node, style in
node.style(style)
var node = node
if let color {
node = node.color(color)
}
.render()
if let styles {
node = node.style(styles)
}
return node.render()
}
}

View File

@@ -0,0 +1,46 @@
public struct LabeledContent: NodeRepresentable {
@usableFromInline
let label: any NodeRepresentable
@usableFromInline
let content: any NodeRepresentable
@usableFromInline
let separator: any NodeRepresentable
@inlinable
public init(
separator: (any NodeRepresentable)? = nil,
@NodeBuilder label: () -> any NodeRepresentable,
@NodeBuilder content: () -> any NodeRepresentable
) {
self.separator = separator ?? "\n"
self.label = label()
self.content = content()
}
@inlinable
public func render() -> String {
Group(separator: separator) {
label
content
}.render()
}
}
public extension LabeledContent {
@inlinable
init(
_ label: String,
separator: (any NodeRepresentable)? = nil,
@NodeBuilder content: () -> any NodeRepresentable
) {
self.init(separator: separator) {
Text(label)
} content: {
content()
}
}
}

View File

@@ -1,34 +0,0 @@
public extension NodeRepresentable {
@inlinable
func repeating(_ count: Int) -> some NodeRepresentable {
RepeatingNode(self, count: count)
}
}
@usableFromInline
struct RepeatingNode: NodeRepresentable {
@usableFromInline
let node: any NodeRepresentable
@usableFromInline
let count: Int
@usableFromInline
init(_ node: any NodeRepresentable, count: Int) {
self.node = node
self.count = count
}
@usableFromInline
func render() -> String {
var string = node.render()
guard count > 0 else { return string }
var count = count - 1
while count > 0 {
string = "\(string)\(node.render())"
count -= 1
}
return string
}
}

View File

@@ -1,41 +0,0 @@
// TODO: Remove init(header:...) initializers.
public struct Section: NodeRepresentable {
@usableFromInline
let node: any NodeRepresentable
@inlinable
public init<N: NodeRepresentable>(@NodeBuilder _ build: () -> N) {
self.node = build()
}
@inlinable
public func render() -> String {
node.render()
}
@inlinable
public init<Content: NodeRepresentable, Separator: NodeRepresentable>(
header: String,
separator: Separator,
@NodeBuilder content: () -> Content
) {
self.init {
Text(header)
separator
content()
}
}
@inlinable
public init<Content: NodeRepresentable>(
header: String,
inline: Bool = false,
@NodeBuilder content: () -> Content
) {
self.init(
header: header,
separator: inline ? Space().eraseToAnyNode() : NewLine().eraseToAnyNode(),
content: content
)
}
}

View File

@@ -0,0 +1,6 @@
extension String: NodeRepresentable {
@inlinable
public func render() -> String {
self
}
}

View File

@@ -13,43 +13,6 @@ public struct Text: NodeRepresentable {
public func render() -> String {
text
}
@inlinable
public func color(_ color: NamedColor) -> Self {
.init(text.applyingColor(color))
}
@inlinable
public func style(_ style: Style) -> Self {
.init(text.applyingStyle(style))
}
}
public struct NewLine: NodeRepresentable {
@usableFromInline
let node: any NodeRepresentable
@inlinable
public init(count: Int = 1) {
self.node = Text("\n").repeating(count)
}
@inlinable
public func render() -> String {
node.render()
}
}
public struct Space: NodeRepresentable {
let node: any NodeRepresentable
public init(count: Int = 1) {
self.node = Text(" ").repeating(count)
}
public func render() -> String {
node.render()
}
}
public extension NodeRepresentable {
@@ -58,12 +21,12 @@ public extension NodeRepresentable {
Text("")
}
static func newLine(count: Int = 1) -> Self where Self == NewLine {
NewLine(count: count)
static func newLine(count: Int = 1) -> Self where Self == AnyNode {
"\n".repeating(count).eraseToAnyNode()
}
static func space(count: Int = 1) -> Self where Self == Space {
Space(count: count)
static func space(count: Int = 1) -> Self where Self == AnyNode {
" ".repeating(count).eraseToAnyNode()
}
}

View File

@@ -1,70 +1,99 @@
@_spi(Internal) import CliDoc
@preconcurrency import Rainbow
import Testing
import XCTest
@Test
func testNodeBuilder() {
let node = AnyNode {
Text("foo").color(.green).style(.bold)
NewLine()
Text("bar").repeating(2)
final class CliDocTests: XCTestCase {
override func setUp() {
super.setUp()
Rainbow.outputTarget = .console
Rainbow.enabled = true
}
let expected = """
\("foo".green.bold)
barbar
"""
#expect(node.render() == expected)
}
@Test(
arguments: [
(true, "foo bar"),
(false, """
foo
bar
""")
]
)
func testSection(
inline: Bool,
expected: String
) {
let node = AnyNode {
Section(
header: "foo",
separator: inline ? Space().eraseToAnyNode() : NewLine().eraseToAnyNode()
) {
Text("bar")
func testStringChecks() {
let expected = "Foo".green.bold
let notexpected = "Foo"
XCTAssert("Foo".green.bold == expected)
XCTAssert("Foo".green.bold != notexpected)
}
func testRepeatingModifier() {
let node = AnyNode {
Text("foo").color(.green).style(.bold)
"\n".repeating(2)
Text("bar").repeating(2, separator: " ")
}
let expected = """
\("foo".green.bold)
bar bar
"""
XCTAssert(node.render() == expected)
}
func testGroup1() {
let arguments = [
(true, "foo bar"),
(false, """
foo
bar
""")
]
for (inline, expected) in arguments {
let node = AnyNode {
Group(separator: inline ? " " : "\n") {
Text("foo")
Text("bar")
}
}
XCTAssert(node.render() == expected)
}
}
print(node.render())
#expect(node.render() == expected)
}
func testHeader() {
let header = Header("Foo")
let expected = "\("Foo".yellow.bold)"
XCTAssert(header.render() == expected)
@Test
func testHeader() {
let header = Header("Foo")
let expected = "\("Foo".yellow.bold)"
#expect(header.render() == expected)
}
@Test
func testGroup() {
let group = Group {
Text("foo")
Text("bar")
let header2 = Header {
"Foo".yellow.bold
}
XCTAssert(header2.render() == expected)
}
print(group.render())
#expect(group.render() == "foo bar")
func testGroup() {
let group = Group {
Text("foo")
Text("bar")
}
let group2 = Group(separator: .newLine()) {
Text("foo")
Text("bar")
XCTAssert(group.render() == "foo bar")
let group2 = Group(separator: "\n") {
Text("foo")
Text("bar")
}
let expected = """
foo
bar
"""
XCTAssert(group2.render() == expected)
}
func testLabeledContent() {
let node = LabeledContent("Foo") {
Text("Bar")
}
.labelStyle(.green)
.labelStyle(.bold)
let expected = """
\("Foo".green.bold)
Bar
"""
XCTAssert(node.render() == expected)
}
let expected = """
foo
bar
"""
#expect(group2.render() == expected)
}