wip: Can't seem to get the node modifiers working properly.
This commit is contained in:
73
Sources/CliDoc2/Builder.swift
Normal file
73
Sources/CliDoc2/Builder.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
@resultBuilder
|
||||
public enum TextBuilder {
|
||||
|
||||
@inlinable
|
||||
public static func buildPartialBlock<N: TextNode>(first: N) -> N {
|
||||
first
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildPartialBlock<N0: TextNode, N1: TextNode>(accumulated: N0, next: N1) -> NodeContainer {
|
||||
.init(nodes: [accumulated, next])
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildArray<N: TextNode>(_ components: [N]) -> NodeContainer {
|
||||
.init(nodes: components)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildBlock<N: TextNode>(_ components: N...) -> NodeContainer {
|
||||
.init(nodes: components)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildEither<N: TextNode, N1: TextNode>(first component: N) -> EitherNode<N, N1> {
|
||||
.first(component)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildEither<N: TextNode, N1: TextNode>(second component: N1) -> EitherNode<N, N1> {
|
||||
.second(component)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildOptional<N: TextNode>(_ component: N?) -> N? {
|
||||
component
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum EitherNode<N: TextNode, N1: TextNode>: TextNode {
|
||||
case first(N)
|
||||
case second(N1)
|
||||
|
||||
public func render() -> String {
|
||||
switch self {
|
||||
case let .first(node): return node.render()
|
||||
case let .second(node): return node.render()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct NodeContainer: TextNode {
|
||||
|
||||
@usableFromInline
|
||||
var nodes: [any TextNode]
|
||||
|
||||
@usableFromInline
|
||||
init(nodes: [any TextNode]) {
|
||||
self.nodes = nodes.reduce(into: [any TextNode]()) { array, next in
|
||||
if let many = next as? NodeContainer {
|
||||
array += many.nodes
|
||||
} else {
|
||||
array.append(next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func render() -> String {
|
||||
nodes.reduce("") { $0 + $1.render() }
|
||||
}
|
||||
}
|
||||
118
Sources/CliDoc2/Modifiers.swift
Normal file
118
Sources/CliDoc2/Modifiers.swift
Normal file
@@ -0,0 +1,118 @@
|
||||
import Rainbow
|
||||
|
||||
public protocol NodeModifier {
|
||||
// swiftlint:disable type_name
|
||||
associatedtype _Body: TextNode
|
||||
typealias Body = _Body
|
||||
// swiftlint:enable type_name
|
||||
|
||||
associatedtype Content: TextNode
|
||||
|
||||
@TextBuilder
|
||||
func render(content: Content) -> Body
|
||||
}
|
||||
|
||||
public extension NodeModifier {
|
||||
|
||||
func concat<T: NodeModifier>(_ modifier: T) -> ConcatModifier<Self, T> {
|
||||
print("Concat: \(type(of: modifier))")
|
||||
return .init(firstModifier: self, secondModifier: modifier)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConcatModifier<M0: NodeModifier, M1: NodeModifier>: NodeModifier where M1.Content == M0.Body {
|
||||
let firstModifier: M0
|
||||
let secondModifier: M1
|
||||
|
||||
public func render(content: M0.Content) -> some TextNode {
|
||||
let firstOutput = firstModifier.render(content: content)
|
||||
return secondModifier.render(content: firstOutput)
|
||||
}
|
||||
}
|
||||
|
||||
public struct ModifiedNode<Content: TextNode, Modifier: NodeModifier> {
|
||||
@usableFromInline
|
||||
let content: Content
|
||||
|
||||
@usableFromInline
|
||||
let modifier: Modifier
|
||||
|
||||
@usableFromInline
|
||||
init(content: Content, modifier: Modifier) {
|
||||
self.content = content
|
||||
self.modifier = modifier
|
||||
}
|
||||
}
|
||||
|
||||
extension ModifiedNode: TextNode where Modifier.Content == Content {
|
||||
public func render() -> String {
|
||||
modifier.render(content: content).render()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func apply<M: NodeModifier>(_ modifier: M) -> ModifiedNode<Content, ConcatModifier<Modifier, M>> {
|
||||
print("ModifiedNode modifier called.")
|
||||
return .init(content: content, modifier: self.modifier.concat(modifier))
|
||||
}
|
||||
}
|
||||
|
||||
public extension TextNode {
|
||||
@inlinable
|
||||
func modifier<M: NodeModifier>(_ modifier: M) -> ModifiedNode<Self, M> {
|
||||
.init(content: self, modifier: modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
struct ColorModifier<Content: TextNode>: NodeModifier {
|
||||
@usableFromInline
|
||||
let color: NamedColor
|
||||
|
||||
@usableFromInline
|
||||
init(color: NamedColor) {
|
||||
self.color = color
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func render(content: Content) -> some TextNode {
|
||||
content.render().applyingColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
public extension TextNode {
|
||||
@inlinable
|
||||
func color(_ color: NamedColor) -> some TextNode {
|
||||
modifier(ColorModifier(color: color))
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
struct StyleModifier<Content: TextNode>: NodeModifier {
|
||||
|
||||
@usableFromInline
|
||||
let styles: [Style]
|
||||
|
||||
@usableFromInline
|
||||
init(styles: [Style]) {
|
||||
self.styles = styles
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func render(content: Content) -> some TextNode {
|
||||
styles.reduce(content.render()) {
|
||||
$0.applyingStyle($1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension TextNode {
|
||||
@inlinable
|
||||
func style(_ styles: Style...) -> some TextNode {
|
||||
modifier(StyleModifier(styles: styles))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func style(_ styles: [Style]) -> some TextNode {
|
||||
modifier(StyleModifier(styles: styles))
|
||||
}
|
||||
}
|
||||
88
Sources/CliDoc2/Modifiers/LabelStyleModifier.swift
Normal file
88
Sources/CliDoc2/Modifiers/LabelStyleModifier.swift
Normal file
@@ -0,0 +1,88 @@
|
||||
import Rainbow
|
||||
|
||||
public extension TextNode {
|
||||
|
||||
@inlinable
|
||||
func labelStyle(color: NamedColor? = nil, styles: Style...) -> some TextNode {
|
||||
labelStyle(color: color, styles: styles)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func labelStyle(color: NamedColor? = nil, styles: [Style]) -> some TextNode {
|
||||
modifier(LabelStyle(color: color, styles: styles))
|
||||
}
|
||||
}
|
||||
|
||||
public extension ModifiedNode where Self: TextNode {
|
||||
@inlinable
|
||||
func labelStyle<M: NodeModifier>(
|
||||
color: NamedColor? = nil, styles: Style...
|
||||
) -> some TextNode where
|
||||
Modifier.Body == Content,
|
||||
M.Content == Modifier.Body
|
||||
{
|
||||
apply(LabelStyle<Content>(color: color, styles: styles))
|
||||
}
|
||||
}
|
||||
|
||||
public struct LabelStyle<Content: TextNode>: NodeModifier {
|
||||
@usableFromInline
|
||||
let color: NamedColor?
|
||||
|
||||
@usableFromInline
|
||||
let styles: [Style]
|
||||
|
||||
@usableFromInline
|
||||
init(color: NamedColor? = nil, styles: [Style] = []) {
|
||||
self.color = color
|
||||
self.styles = styles
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func render(content: Content) -> some TextNode {
|
||||
print("Handling node: \(type(of: content))")
|
||||
return handleNode(content)
|
||||
}
|
||||
|
||||
@TextBuilder
|
||||
@usableFromInline
|
||||
func handleNode<N: TextNode>(_ content: N) -> some TextNode {
|
||||
if let label = content as? Label {
|
||||
handleLabel(label)
|
||||
} else if let container = content as? NodeContainer {
|
||||
handleContainer(container)
|
||||
} else if let group = content as? Group {
|
||||
handleGroup(group)
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func handleLabel(_ label: Label) -> Label {
|
||||
var label = label
|
||||
if let color {
|
||||
label.node = label.node.color(color)
|
||||
}
|
||||
label.node = label.node.style(styles)
|
||||
return label
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func handleContainer(_ container: NodeContainer) -> NodeContainer {
|
||||
var container = container
|
||||
for (idx, node) in container.nodes.enumerated() {
|
||||
container.nodes[idx] = handleNode(node)
|
||||
}
|
||||
return container
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func handleGroup(_ group: Group) -> Group {
|
||||
var group = group
|
||||
for (idx, node) in group.content.enumerated() {
|
||||
group.content[idx] = handleNode(node)
|
||||
}
|
||||
return group
|
||||
}
|
||||
}
|
||||
@@ -1,149 +1,24 @@
|
||||
import Rainbow
|
||||
|
||||
public protocol Node {
|
||||
public protocol TextNode {
|
||||
func render() -> String
|
||||
}
|
||||
|
||||
extension String: Node {
|
||||
public extension TextNode {
|
||||
func map<T: TextNode>(_ transform: (Self) -> T) -> T {
|
||||
transform(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension String: TextNode {
|
||||
public func render() -> String {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@resultBuilder
|
||||
enum NodeBuilder {
|
||||
|
||||
public static func buildPartialBlock<N: Node>(first: N) -> N {
|
||||
first
|
||||
}
|
||||
|
||||
static func buildPartialBlock<N0: Node, N1: Node>(accumulated: N0, next: N1) -> ManyNode {
|
||||
.init(nodes: [accumulated, next])
|
||||
}
|
||||
|
||||
public static func buildArray<N: Node>(_ components: [N]) -> ManyNode {
|
||||
.init(nodes: components)
|
||||
}
|
||||
|
||||
static func buildBlock<N: Node>(_ components: N...) -> ManyNode {
|
||||
.init(nodes: components)
|
||||
}
|
||||
|
||||
static func buildEither<N: Node>(first component: N) -> N {
|
||||
component
|
||||
}
|
||||
|
||||
static func buildEither<N: Node>(second component: N) -> N {
|
||||
component
|
||||
}
|
||||
|
||||
static func buildOptional<N: Node>(_ component: N?) -> any Node {
|
||||
component
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Optional: Node where Wrapped: Node {
|
||||
extension Optional: TextNode where Wrapped: TextNode {
|
||||
public func render() -> String {
|
||||
guard let node = self else { return "" }
|
||||
return node.render()
|
||||
}
|
||||
}
|
||||
|
||||
struct ManyNode: Node {
|
||||
|
||||
var nodes: [any Node]
|
||||
|
||||
init(nodes: [any Node]) {
|
||||
self.nodes = nodes.reduce(into: [any Node]()) { array, next in
|
||||
if let many = next as? ManyNode {
|
||||
array += many.nodes
|
||||
} else {
|
||||
array.append(next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
nodes.reduce("") { $0 + $1.render() }
|
||||
}
|
||||
}
|
||||
|
||||
struct Group: Node {
|
||||
var content: [any Node]
|
||||
var separator: any Node
|
||||
|
||||
init(
|
||||
content: [any Node],
|
||||
separator: any Node = "\n"
|
||||
) {
|
||||
self.content = content
|
||||
self.separator = separator
|
||||
}
|
||||
|
||||
init(
|
||||
separator: any Node = "\n",
|
||||
@NodeBuilder content: () -> any Node
|
||||
) {
|
||||
let content = content()
|
||||
if let many = content as? ManyNode {
|
||||
self.content = many.nodes
|
||||
} else {
|
||||
self.content = [content]
|
||||
}
|
||||
self.separator = separator
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
content.reduce("") {
|
||||
$0 + $1.render() + separator.render()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ColorNode: Node {
|
||||
let color: NamedColor
|
||||
let node: any Node
|
||||
|
||||
func render() -> String {
|
||||
node.render().applyingColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
extension Node {
|
||||
func color(_ color: NamedColor) -> some Node {
|
||||
ColorNode(color: color, node: self)
|
||||
}
|
||||
}
|
||||
|
||||
struct Label: Node {
|
||||
var node: any Node
|
||||
|
||||
init(@NodeBuilder _ content: () -> any Node) {
|
||||
self.node = content()
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
node.render()
|
||||
}
|
||||
}
|
||||
|
||||
struct Note: Node {
|
||||
var label: any Node
|
||||
var content: any Node
|
||||
var separator: any Node = " "
|
||||
|
||||
init(
|
||||
separator: any Node = " ",
|
||||
@NodeBuilder _ label: () -> any Node,
|
||||
@NodeBuilder content: () -> any Node
|
||||
) {
|
||||
self.separator = separator
|
||||
self.label = label()
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
func render() -> String {
|
||||
Group(content: [label, content], separator: separator).render()
|
||||
}
|
||||
}
|
||||
|
||||
105
Sources/CliDoc2/Nodes.swift
Normal file
105
Sources/CliDoc2/Nodes.swift
Normal file
@@ -0,0 +1,105 @@
|
||||
public struct AnyTextNode: TextNode {
|
||||
@usableFromInline
|
||||
var makeContent: () -> String
|
||||
|
||||
@inlinable
|
||||
public init<T: TextNode>(_ node: T) {
|
||||
self.makeContent = node.render
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func render() -> String {
|
||||
makeContent()
|
||||
}
|
||||
}
|
||||
|
||||
public extension TextNode {
|
||||
func eraseToAnyTextNode() -> AnyTextNode {
|
||||
.init(self)
|
||||
}
|
||||
}
|
||||
|
||||
public struct Group: TextNode {
|
||||
@usableFromInline
|
||||
var content: [any TextNode]
|
||||
|
||||
@usableFromInline
|
||||
var separator: any TextNode
|
||||
|
||||
@usableFromInline
|
||||
init(
|
||||
content: [any TextNode],
|
||||
separator: any TextNode = "\n"
|
||||
) {
|
||||
self.content = content
|
||||
self.separator = separator
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public init(
|
||||
separator: any TextNode = "\n",
|
||||
@TextBuilder content: () -> any TextNode
|
||||
) {
|
||||
let content = content()
|
||||
if let many = content as? NodeContainer {
|
||||
self.content = many.nodes
|
||||
} else {
|
||||
self.content = [content]
|
||||
}
|
||||
self.separator = separator
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func render() -> String {
|
||||
content.reduce("") {
|
||||
$0 + $1.render() + separator.render()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Label: TextNode {
|
||||
@usableFromInline
|
||||
var node: any TextNode
|
||||
|
||||
@inlinable
|
||||
public init(@TextBuilder _ content: () -> any TextNode) {
|
||||
self.node = content()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public init(_ node: any TextNode) {
|
||||
self.node = node
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func render() -> String {
|
||||
node.render()
|
||||
}
|
||||
}
|
||||
|
||||
public struct Note: TextNode {
|
||||
@usableFromInline
|
||||
var label: any TextNode
|
||||
|
||||
@usableFromInline
|
||||
var content: any TextNode
|
||||
|
||||
@usableFromInline
|
||||
var separator: any TextNode = " "
|
||||
|
||||
@inlinable
|
||||
public init(
|
||||
separator: any TextNode = " ",
|
||||
@TextBuilder _ label: () -> any TextNode,
|
||||
@TextBuilder content: () -> any TextNode
|
||||
) {
|
||||
self.separator = separator
|
||||
self.label = label()
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public func render() -> String {
|
||||
Group(content: [label, content], separator: separator).render()
|
||||
}
|
||||
}
|
||||
@@ -12,20 +12,24 @@ let setupRainbow: Bool = {
|
||||
func testGroup() {
|
||||
#expect(setupRainbow)
|
||||
let group = Group {
|
||||
Label { "Foo:" }.color(.blue)
|
||||
Label { "Foo:" }
|
||||
"Bar"
|
||||
"Baz"
|
||||
Note { "Bang:" } content: { "boom" }
|
||||
if setupRainbow {
|
||||
Label("Hello, rainbow").color(.blue)
|
||||
} else {
|
||||
Label("No color for you!").color(.green)
|
||||
Label("No color for you!").color(.red)
|
||||
}
|
||||
}.color(.green)
|
||||
}
|
||||
.color(.green)
|
||||
.style(.italic)
|
||||
.labelStyle(color: .blue, styles: .bold)
|
||||
|
||||
print(type(of: group))
|
||||
print(group.render())
|
||||
|
||||
let note = Note { "Bang:" } content: { "boom" }
|
||||
print(note.render())
|
||||
print(type(of: note.label))
|
||||
// let note = Note { "Bang:" } content: { "boom" }
|
||||
// print(note.render())
|
||||
// print(type(of: note.label))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user