diff --git a/Sources/CliDoc2/Builder.swift b/Sources/CliDoc2/Builder.swift index 891a993..3039cfd 100644 --- a/Sources/CliDoc2/Builder.swift +++ b/Sources/CliDoc2/Builder.swift @@ -42,10 +42,10 @@ public enum EitherNode: TextNode { case first(N) case second(N1) - public func render() -> String { + public var body: some TextNode { switch self { - case let .first(node): return node.render() - case let .second(node): return node.render() + case let .first(node): return node.eraseToAnyTextNode() + case let .second(node): return node.eraseToAnyTextNode() } } } @@ -67,7 +67,7 @@ public struct NodeContainer: TextNode { } @inlinable - public func render() -> String { + public var body: some TextNode { nodes.reduce("") { $0 + $1.render() } } } diff --git a/Sources/CliDoc2/Modifiers/ColorModifier.swift b/Sources/CliDoc2/Modifiers/ColorModifier.swift new file mode 100644 index 0000000..a37c13f --- /dev/null +++ b/Sources/CliDoc2/Modifiers/ColorModifier.swift @@ -0,0 +1,24 @@ +import Rainbow + +public extension TextNode { + @inlinable + func color(_ color: NamedColor) -> some TextNode { + modifier(ColorModifier(color: color)) + } +} + +@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) + } +} diff --git a/Sources/CliDoc2/Modifiers/LabelStyleModifier.swift b/Sources/CliDoc2/Modifiers/LabelStyleModifier.swift index 6cc7393..618dd9a 100644 --- a/Sources/CliDoc2/Modifiers/LabelStyleModifier.swift +++ b/Sources/CliDoc2/Modifiers/LabelStyleModifier.swift @@ -1,31 +1,16 @@ 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 { + func labelStyle(color: NamedColor? = nil, styles: [Style] = []) -> some TextNode where Self == Label { 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)) + func labelStyle(color: NamedColor? = nil, styles: Style...) -> some TextNode where Self == Label { + labelStyle(color: color, styles: styles) } } -public struct LabelStyle: NodeModifier { +public struct LabelStyle: NodeModifier { @usableFromInline let color: NamedColor? @@ -39,50 +24,12 @@ public struct LabelStyle: NodeModifier { } @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 + public func render(content: Label) -> some TextNode { + var label: any TextNode = content.content + label = label.style(styles) if let color { - label.node = label.node.color(color) + label = label.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 + return Label { label.eraseToAnyTextNode() } } } diff --git a/Sources/CliDoc2/Modifiers/StyleModifier.swift b/Sources/CliDoc2/Modifiers/StyleModifier.swift new file mode 100644 index 0000000..74c4e8d --- /dev/null +++ b/Sources/CliDoc2/Modifiers/StyleModifier.swift @@ -0,0 +1,32 @@ +import Rainbow + +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)) + } +} + +@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) + } + } +} diff --git a/Sources/CliDoc2/Node.swift b/Sources/CliDoc2/Node.swift deleted file mode 100644 index 8f0e041..0000000 --- a/Sources/CliDoc2/Node.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Rainbow - -public protocol TextNode { - func render() -> String -} - -public extension TextNode { - func map(_ transform: (Self) -> T) -> T { - transform(self) - } -} - -extension String: TextNode { - public func render() -> String { - self - } -} - -extension Optional: TextNode where Wrapped: TextNode { - public func render() -> String { - guard let node = self else { return "" } - return node.render() - } -} diff --git a/Sources/CliDoc2/Modifiers.swift b/Sources/CliDoc2/NodeModifier.swift similarity index 56% rename from Sources/CliDoc2/Modifiers.swift rename to Sources/CliDoc2/NodeModifier.swift index 85e44df..7a9b424 100644 --- a/Sources/CliDoc2/Modifiers.swift +++ b/Sources/CliDoc2/NodeModifier.swift @@ -15,7 +15,6 @@ public protocol NodeModifier { public extension NodeModifier { func concat(_ modifier: T) -> ConcatModifier { - print("Concat: \(type(of: modifier))") return .init(firstModifier: self, secondModifier: modifier) } } @@ -45,74 +44,25 @@ public struct ModifiedNode { } extension ModifiedNode: TextNode where Modifier.Content == Content { - public func render() -> String { - modifier.render(content: content).render() + public var body: some TextNode { + modifier.render(content: content) } @inlinable func apply(_ modifier: M) -> ModifiedNode> { - print("ModifiedNode modifier called.") return .init(content: content, modifier: self.modifier.concat(modifier)) } } +extension ModifiedNode: NodeRepresentable where Self: TextNode { + public func render() -> String { + body.render() + } +} + 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/Nodes.swift b/Sources/CliDoc2/Nodes.swift deleted file mode 100644 index 928ee00..0000000 --- a/Sources/CliDoc2/Nodes.swift +++ /dev/null @@ -1,105 +0,0 @@ -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/Sources/CliDoc2/Nodes/Group.swift b/Sources/CliDoc2/Nodes/Group.swift new file mode 100644 index 0000000..44d9b77 --- /dev/null +++ b/Sources/CliDoc2/Nodes/Group.swift @@ -0,0 +1,37 @@ +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 var body: some TextNode { + content.reduce("") { + $0 + $1.render() + separator.render() + } + } +} diff --git a/Sources/CliDoc2/Nodes/Label.swift b/Sources/CliDoc2/Nodes/Label.swift new file mode 100644 index 0000000..34cd5e6 --- /dev/null +++ b/Sources/CliDoc2/Nodes/Label.swift @@ -0,0 +1,19 @@ +public struct Label: TextNode { + @usableFromInline + let content: Content + + @inlinable + public init(@TextBuilder _ content: () -> Content) { + self.content = content() + } + + @inlinable + public init(_ content: Content) { + self.content = content + } + + @inlinable + public var body: some TextNode { + content + } +} diff --git a/Sources/CliDoc2/Nodes/Note.swift b/Sources/CliDoc2/Nodes/Note.swift new file mode 100644 index 0000000..009283b --- /dev/null +++ b/Sources/CliDoc2/Nodes/Note.swift @@ -0,0 +1,51 @@ +import Rainbow + +public struct Note: TextNode { + @usableFromInline + let label: Label + + @usableFromInline + let content: Content + + @usableFromInline + var separator: any TextNode = " " + + @inlinable + public init( + separator: any TextNode = " ", + @TextBuilder _ label: () -> Label, + @TextBuilder content: () -> Content + ) { + self.separator = separator + self.label = label() + self.content = content() + } + + @inlinable + public var body: some TextNode { + Group(content: [label, content], separator: separator) + } +} + +public extension Note where Label == String { + + @inlinable + init( + separator: any TextNode = " ", + _ label: String = "NOTE:".yellow.bold, + @TextBuilder content: () -> Content + ) { + self.separator = separator + self.label = label + self.content = content() + } + + static func important( + separator: any TextNode = " ", + _ label: String = "IMPORTANT NOTE:".red.underline, + @TextBuilder content: () -> Content + ) { + self.init(separator: separator, label, content: content) + } + +} diff --git a/Sources/CliDoc2/Nodes/ShellCommand.swift b/Sources/CliDoc2/Nodes/ShellCommand.swift new file mode 100644 index 0000000..13c6263 --- /dev/null +++ b/Sources/CliDoc2/Nodes/ShellCommand.swift @@ -0,0 +1,21 @@ +public struct ShellCommand: TextNode { + + @usableFromInline + var symbol: any TextNode + + @usableFromInline + var content: Content + + @inlinable + public init( + symbol: any TextNode = "$", + @TextBuilder content: () -> Content + ) { + self.symbol = symbol + self.content = content() + } + + public var body: some TextNode { + Group(content: [symbol, content], separator: " ") + } +} diff --git a/Sources/CliDoc2/TextNode.swift b/Sources/CliDoc2/TextNode.swift new file mode 100644 index 0000000..15cdbb5 --- /dev/null +++ b/Sources/CliDoc2/TextNode.swift @@ -0,0 +1,61 @@ +public protocol NodeRepresentable { + func render() -> String +} + +public protocol TextNode: NodeRepresentable { + // swiftlint:disable type_name + associatedtype _Body: TextNode + typealias Body = _Body + // swiftlint:enable type_name + + var body: Body { get } +} + +public extension TextNode { + func render() -> String { + body.render() + } +} + +extension String: NodeRepresentable { + public func render() -> String { + self + } +} + +extension String: TextNode { + public var body: some TextNode { + self + } +} + +public struct AnyTextNode: TextNode { + let makeString: () -> String + + public init(_ node: N) { + self.makeString = node.render + } + + public var body: some TextNode { makeString() } +} + +public extension TextNode { + func eraseToAnyTextNode() -> AnyTextNode { + AnyTextNode(self) + } +} + +extension Optional: TextNode where Wrapped: TextNode { + public var body: some TextNode { + guard let node = self else { return "".eraseToAnyTextNode() } + return node.eraseToAnyTextNode() + } +} + +extension Optional: NodeRepresentable where Wrapped: NodeRepresentable { + + public func render() -> String { + guard let node = self else { return "" } + return node.render() + } +} diff --git a/Tests/CliDoc2Tests/CliDoc2Tests.swift b/Tests/CliDoc2Tests/CliDoc2Tests.swift index f305464..f7bb0e6 100644 --- a/Tests/CliDoc2Tests/CliDoc2Tests.swift +++ b/Tests/CliDoc2Tests/CliDoc2Tests.swift @@ -24,7 +24,6 @@ func testGroup() { } .color(.green) .style(.italic) - .labelStyle(color: .blue, styles: .bold) print(type(of: group)) print(group.render())