From f73ded3314b9fbfee9c97eb026118e4bb5113923 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Tue, 3 Dec 2024 15:40:07 -0500 Subject: [PATCH] wip: Can't seem to get the node modifiers working properly. --- Sources/CliDoc2/Builder.swift | 73 +++++++++ Sources/CliDoc2/Modifiers.swift | 118 +++++++++++++++ .../Modifiers/LabelStyleModifier.swift | 88 +++++++++++ Sources/CliDoc2/Node.swift | 143 ++---------------- Sources/CliDoc2/Nodes.swift | 105 +++++++++++++ Tests/CliDoc2Tests/CliDoc2Tests.swift | 16 +- 6 files changed, 403 insertions(+), 140 deletions(-) create mode 100644 Sources/CliDoc2/Builder.swift create mode 100644 Sources/CliDoc2/Modifiers.swift create mode 100644 Sources/CliDoc2/Modifiers/LabelStyleModifier.swift create mode 100644 Sources/CliDoc2/Nodes.swift diff --git a/Sources/CliDoc2/Builder.swift b/Sources/CliDoc2/Builder.swift new file mode 100644 index 0000000..891a993 --- /dev/null +++ b/Sources/CliDoc2/Builder.swift @@ -0,0 +1,73 @@ +@resultBuilder +public enum TextBuilder { + + @inlinable + public static func buildPartialBlock(first: N) -> N { + first + } + + @inlinable + public static func buildPartialBlock(accumulated: N0, next: N1) -> NodeContainer { + .init(nodes: [accumulated, next]) + } + + @inlinable + public static func buildArray(_ components: [N]) -> NodeContainer { + .init(nodes: components) + } + + @inlinable + public static func buildBlock(_ components: N...) -> NodeContainer { + .init(nodes: components) + } + + @inlinable + public static func buildEither(first component: N) -> EitherNode { + .first(component) + } + + @inlinable + public static func buildEither(second component: N1) -> EitherNode { + .second(component) + } + + @inlinable + public static func buildOptional(_ component: N?) -> N? { + component + } + +} + +public enum EitherNode: 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() } + } +} diff --git a/Sources/CliDoc2/Modifiers.swift b/Sources/CliDoc2/Modifiers.swift new file mode 100644 index 0000000..85e44df --- /dev/null +++ b/Sources/CliDoc2/Modifiers.swift @@ -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(_ modifier: T) -> ConcatModifier { + print("Concat: \(type(of: modifier))") + return .init(firstModifier: self, secondModifier: modifier) + } +} + +public struct ConcatModifier: 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 { + @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(_ modifier: M) -> ModifiedNode> { + print("ModifiedNode modifier called.") + return .init(content: content, modifier: self.modifier.concat(modifier)) + } +} + +public extension TextNode { + @inlinable + func modifier(_ modifier: M) -> ModifiedNode { + .init(content: self, modifier: modifier) + } +} + +@usableFromInline +struct ColorModifier: 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: 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)) + } +} diff --git a/Sources/CliDoc2/Modifiers/LabelStyleModifier.swift b/Sources/CliDoc2/Modifiers/LabelStyleModifier.swift new file mode 100644 index 0000000..6cc7393 --- /dev/null +++ b/Sources/CliDoc2/Modifiers/LabelStyleModifier.swift @@ -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( + color: NamedColor? = nil, styles: Style... + ) -> some TextNode where + Modifier.Body == Content, + M.Content == Modifier.Body + { + apply(LabelStyle(color: color, styles: styles)) + } +} + +public struct LabelStyle: 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(_ 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 + } +} diff --git a/Sources/CliDoc2/Node.swift b/Sources/CliDoc2/Node.swift index d903ae7..8f0e041 100644 --- a/Sources/CliDoc2/Node.swift +++ b/Sources/CliDoc2/Node.swift @@ -1,149 +1,24 @@ import Rainbow -public protocol Node { +public protocol TextNode { func render() -> String } -extension String: Node { +public extension TextNode { + func map(_ transform: (Self) -> T) -> T { + transform(self) + } +} + +extension String: TextNode { public func render() -> String { self } } -@resultBuilder -enum NodeBuilder { - - public static func buildPartialBlock(first: N) -> N { - first - } - - static func buildPartialBlock(accumulated: N0, next: N1) -> ManyNode { - .init(nodes: [accumulated, next]) - } - - public static func buildArray(_ components: [N]) -> ManyNode { - .init(nodes: components) - } - - static func buildBlock(_ components: N...) -> ManyNode { - .init(nodes: components) - } - - static func buildEither(first component: N) -> N { - component - } - - static func buildEither(second component: N) -> N { - component - } - - static func buildOptional(_ 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() - } -} diff --git a/Sources/CliDoc2/Nodes.swift b/Sources/CliDoc2/Nodes.swift new file mode 100644 index 0000000..928ee00 --- /dev/null +++ b/Sources/CliDoc2/Nodes.swift @@ -0,0 +1,105 @@ +public struct AnyTextNode: TextNode { + @usableFromInline + var makeContent: () -> String + + @inlinable + public init(_ 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() + } +} diff --git a/Tests/CliDoc2Tests/CliDoc2Tests.swift b/Tests/CliDoc2Tests/CliDoc2Tests.swift index dadfdb8..f305464 100644 --- a/Tests/CliDoc2Tests/CliDoc2Tests.swift +++ b/Tests/CliDoc2Tests/CliDoc2Tests.swift @@ -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)) }