From 22dfc6ce51afa9adb4ab30f81d9a37e70fcd7d74 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Mon, 2 Dec 2024 17:04:28 -0500 Subject: [PATCH] feat: Initial commit --- .editorconfig | 7 +++ .gitignore | 8 +++ .swiftformat | 11 ++++ .swiftlint.yml | 10 ++++ Package.resolved | 15 ++++++ Package.swift | 26 +++++++++ Sources/CliDoc/Modifiers/LabelColor.swift | 59 ++++++++++++++++++++ Sources/CliDoc/Modifiers/LabelStyle.swift | 21 ++++++++ Sources/CliDoc/Node.swift | 27 ++++++++++ Sources/CliDoc/NodeBuilder.swift | 36 +++++++++++++ Sources/CliDoc/NodeModifier.swift | 59 ++++++++++++++++++++ Sources/CliDoc/Nodes/AnyNode.swift | 18 +++++++ Sources/CliDoc/Nodes/Colored.swift | 18 +++++++ Sources/CliDoc/Nodes/Group.swift | 28 ++++++++++ Sources/CliDoc/Nodes/Label.swift | 45 ++++++++++++++++ Sources/CliDoc/Nodes/Note.swift | 31 +++++++++++ Sources/CliDoc/Nodes/ShellCommand.swift | 20 +++++++ Tests/CliDocTests/CliDocTests.swift | 66 +++++++++++++++++++++++ 18 files changed, 505 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .swiftformat create mode 100644 .swiftlint.yml create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 Sources/CliDoc/Modifiers/LabelColor.swift create mode 100644 Sources/CliDoc/Modifiers/LabelStyle.swift create mode 100644 Sources/CliDoc/Node.swift create mode 100644 Sources/CliDoc/NodeBuilder.swift create mode 100644 Sources/CliDoc/NodeModifier.swift create mode 100644 Sources/CliDoc/Nodes/AnyNode.swift create mode 100644 Sources/CliDoc/Nodes/Colored.swift create mode 100644 Sources/CliDoc/Nodes/Group.swift create mode 100644 Sources/CliDoc/Nodes/Label.swift create mode 100644 Sources/CliDoc/Nodes/Note.swift create mode 100644 Sources/CliDoc/Nodes/ShellCommand.swift create mode 100644 Tests/CliDocTests/CliDocTests.swift diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7cfbe01 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*.swift] +indent_style = space +indent_size = 2 +tab_width = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..08f338e --- /dev/null +++ b/.swiftformat @@ -0,0 +1,11 @@ +--self init-only +--indent 2 +--ifdef indent +--trimwhitespace always +--wraparguments before-first +--wrapparameters before-first +--wrapcollections preserve +--wrapconditions after-first +--typeblanklines preserve +--commas inline +--stripunusedargs closure-only diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..cd25531 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,10 @@ +disabled_rules: + - closing_brace + - fuction_body_length + - opening_brace + +included: + - Sources + - Tests + +ignore_multiline_statement_conditions: true diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..00198e1 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "ce5e40ef3be5875abe47bbfe26413215dee9b937de7df5293343757c7476d755", + "pins" : [ + { + "identity" : "rainbow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Rainbow", + "state" : { + "revision" : "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3", + "version" : "4.0.1" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..5adda92 --- /dev/null +++ b/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "swift-cli-doc", + products: [ + .library(name: "CliDoc", targets: ["CliDoc"]) + ], + dependencies: [ + .package(url: "https://github.com/onevcat/Rainbow", from: "4.0.0") + ], + targets: [ + .target( + name: "CliDoc", + dependencies: [ + .product(name: "Rainbow", package: "Rainbow") + ] + + ), + .testTarget( + name: "CliDocTests", + dependencies: ["CliDoc"] + ) + ] +) diff --git a/Sources/CliDoc/Modifiers/LabelColor.swift b/Sources/CliDoc/Modifiers/LabelColor.swift new file mode 100644 index 0000000..922e6bf --- /dev/null +++ b/Sources/CliDoc/Modifiers/LabelColor.swift @@ -0,0 +1,59 @@ +import Rainbow + +public extension Group { + func labelColor(_ color: NamedColor) -> some Node { + modifier(GroupLabelModifier(color: color)) + } +} + +public extension Node where Self.Body == Group { + func labelColor(_ color: NamedColor) -> some Node { + body.modifier(GroupLabelModifier(color: color)) + } +} + +public extension Node where Self == Label { + func labelColor(_ color: NamedColor) -> some Node { + modifier(LabelColorModifier(color: color)) + } +} + +public extension Note where Label == CliDoc.Label { + func labelColor(_ color: NamedColor) -> some Node { + var node = self + node.label.color = color + return node + } +} + +struct LabelColorModifier: NodeModifier { + let color: NamedColor + + func render(content: Label) -> some Node { + var label = content + label.color = color + return label + } +} + +struct GroupLabelModifier: NodeModifier { + let color: NamedColor + + func render(content: Group) -> some Node { + var group = content + applyLabelColor(&group) + return group + } + + private func applyLabelColor(_ group: inout Group) { + for (idx, node) in group.nodes.enumerated() { + if var label = node as? Label { + label.color = color + group.nodes[idx] = label + } else if var nestedGroup = node as? Group { + applyLabelColor(&nestedGroup) + group.nodes[idx] = nestedGroup + } + } + } +} diff --git a/Sources/CliDoc/Modifiers/LabelStyle.swift b/Sources/CliDoc/Modifiers/LabelStyle.swift new file mode 100644 index 0000000..7f718bb --- /dev/null +++ b/Sources/CliDoc/Modifiers/LabelStyle.swift @@ -0,0 +1,21 @@ +import Rainbow + +// public extension Node { +// func labelStyel(_ styles: Style...) -> any Node {} +// } + +public extension Node where Self == Label { + func labelStyle(_ styles: Style...) -> some Node { + modifier(LabelStyleModifier(styles: styles)) + } +} + +struct LabelStyleModifier: NodeModifier { + let styles: [Style] + + func render(content: Label) -> some Node { + var label = content + label.styles = styles + return label + } +} diff --git a/Sources/CliDoc/Node.swift b/Sources/CliDoc/Node.swift new file mode 100644 index 0000000..5071870 --- /dev/null +++ b/Sources/CliDoc/Node.swift @@ -0,0 +1,27 @@ +public protocol NodeRepresentable { + func render() -> String +} + +public protocol Node: NodeRepresentable { + // swiftlint:disable type_name + associatedtype _Body: Node + typealias Body = _Body + // swiftlint:enable type_name + + @NodeBuilder + var body: Body { get } +} + +public extension Node { + func render() -> String { + body.render() + } +} + +extension String: NodeRepresentable { + public func render() -> String { self } +} + +extension String: Node { + public var body: some Node { self } +} diff --git a/Sources/CliDoc/NodeBuilder.swift b/Sources/CliDoc/NodeBuilder.swift new file mode 100644 index 0000000..3cefab3 --- /dev/null +++ b/Sources/CliDoc/NodeBuilder.swift @@ -0,0 +1,36 @@ +@resultBuilder +public enum NodeBuilder { + + public static func buildPartialBlock(first: N) -> N { + first + } + + public static func buildArray(_ components: [N]) -> Group { + .init(components) + } + + public static func buildOptional(_ component: N?) -> OptionalNode { + .init(component: component) + } + + public static func buildEither(first component: N) -> N { + component + } + + public static func buildEither(second component: N) -> N { + component + } + + public static func buildPartialBlock(accumulated: N0, next: N1) -> Group { + .init([accumulated, next]) + } + +} + +public struct OptionalNode: Node { + let component: N? + + public var body: some Node { + component?.render() ?? "" + } +} diff --git a/Sources/CliDoc/NodeModifier.swift b/Sources/CliDoc/NodeModifier.swift new file mode 100644 index 0000000..2e5fbbd --- /dev/null +++ b/Sources/CliDoc/NodeModifier.swift @@ -0,0 +1,59 @@ +public protocol NodeModifier { + associatedtype Body: Node + // swiftlint:disable type_name + associatedtype _Content: Node + typealias Content = _Content + // swiftlint:enable type_name + + func render(content: Content) -> Body +} + +public extension NodeModifier { + func modifier( + _ modifier: T + ) -> ModifiedNode where T.Content == Body { + .init(content: self, modifier: modifier) + } +} + +public extension Node { + + func modifier(_ modifier: T) -> ModifiedNode { + .init(content: self, modifier: modifier) + } +} + +public struct ModifiedNode { + var content: Content + var modifier: Modifier +} + +extension ModifiedNode: NodeRepresentable where Content: NodeRepresentable, + Modifier: NodeModifier, + Modifier.Content == Content +{ + public func render() -> String { + modifier.render(content: content).render() + } +} + +extension ModifiedNode: Node where Content: Node, + Modifier: NodeModifier, + Modifier.Content == Content +{ + public var body: some Node { + content + } +} + +extension ModifiedNode: NodeModifier where + Content: NodeModifier, + Modifier: NodeModifier, + Content.Body == Modifier.Content, + Content: Node +{ + public func render(content: Content) -> some Node { + let body = content.body + return modifier.render(content: body).render() + } +} diff --git a/Sources/CliDoc/Nodes/AnyNode.swift b/Sources/CliDoc/Nodes/AnyNode.swift new file mode 100644 index 0000000..a53c14f --- /dev/null +++ b/Sources/CliDoc/Nodes/AnyNode.swift @@ -0,0 +1,18 @@ +/// A type erased node. +public struct AnyNode: Node { + private var renderNode: () -> String + + public init(_ node: N) { + self.renderNode = node.render + } + + public var body: some Node { + renderNode() + } +} + +public extension Node { + func eraseToAnyNode() -> AnyNode { + .init(self) + } +} diff --git a/Sources/CliDoc/Nodes/Colored.swift b/Sources/CliDoc/Nodes/Colored.swift new file mode 100644 index 0000000..9f1c5b4 --- /dev/null +++ b/Sources/CliDoc/Nodes/Colored.swift @@ -0,0 +1,18 @@ +import Rainbow + +public struct Colored: Node { + var color: NamedColor + var node: any Node + + public init( + color: NamedColor, + @NodeBuilder build: () -> any Node + ) { + self.color = color + self.node = build() + } + + public var body: some Node { + node.render().applyingColor(color) + } +} diff --git a/Sources/CliDoc/Nodes/Group.swift b/Sources/CliDoc/Nodes/Group.swift new file mode 100644 index 0000000..f61d4e8 --- /dev/null +++ b/Sources/CliDoc/Nodes/Group.swift @@ -0,0 +1,28 @@ +/// A group container holding one or more nodes. +public struct Group: Node { + var nodes: [any Node] + var separator: String + + init(_ nodes: [any Node], separator: String = "\n") { + self.nodes = nodes + self.separator = separator + } + + public init( + separator: String = " ", + @NodeBuilder build: () -> any Node + ) { + let node = build() + if var group = node as? Self { + group.separator = separator + self = group + } else { + self.init([node], separator: separator) + } + } + + public var body: some Node { + nodes.map { $0.render() }.joined(separator: separator) + } + +} diff --git a/Sources/CliDoc/Nodes/Label.swift b/Sources/CliDoc/Nodes/Label.swift new file mode 100644 index 0000000..55b906d --- /dev/null +++ b/Sources/CliDoc/Nodes/Label.swift @@ -0,0 +1,45 @@ +import Rainbow + +public struct Label: Node { + + var color: NamedColor? + var styles: [Style] + let node: any Node + + public init( + _ label: String, + color: NamedColor? = nil, + style styles: Style... + ) { + self.color = color + self.node = label + self.styles = styles + } + + public init( + _ label: String, + color: NamedColor? = nil, + style styles: [Style] = [] + ) { + self.color = color + self.node = label + self.styles = styles + } + + public init( + color: NamedColor? = nil, + styles: [Style] = [], + @NodeBuilder _ build: () -> any Node + ) { + self.color = color + self.styles = styles + self.node = build() + } + + public var body: some Node { + let output = styles.reduce(node.render()) { $0.applyingStyle($1) } + guard let color else { return output } + return output.applyingColor(color) + } + +} diff --git a/Sources/CliDoc/Nodes/Note.swift b/Sources/CliDoc/Nodes/Note.swift new file mode 100644 index 0000000..a7fa812 --- /dev/null +++ b/Sources/CliDoc/Nodes/Note.swift @@ -0,0 +1,31 @@ +public struct Note: Node { + + var separator: String + var label: Label + var content: Content + + public init( + separator: String = " ", + @NodeBuilder label: () -> Label, + @NodeBuilder content: () -> Content + ) { + self.separator = separator + self.label = label() + self.content = content() + } + + public var body: some Node { + Group([label, content], separator: separator) + } +} + +public extension Note where Label == CliDoc.Label { + + init( + separator: String = " ", + label: String = "NOTE:", + @NodeBuilder content: () -> Content + ) { + self.init(separator: separator, label: { CliDoc.Label(label) }, content: content) + } +} diff --git a/Sources/CliDoc/Nodes/ShellCommand.swift b/Sources/CliDoc/Nodes/ShellCommand.swift new file mode 100644 index 0000000..a261831 --- /dev/null +++ b/Sources/CliDoc/Nodes/ShellCommand.swift @@ -0,0 +1,20 @@ +public struct ShellCommand: Node { + + public var symbol: String + public var content: Content + + public init( + symbol: String = "$", + @NodeBuilder content: () -> Content + ) { + self.symbol = symbol + self.content = content() + } + + public var body: some Node { + Group(separator: " ") { + symbol + content + } + } +} diff --git a/Tests/CliDocTests/CliDocTests.swift b/Tests/CliDocTests/CliDocTests.swift new file mode 100644 index 0000000..979bef0 --- /dev/null +++ b/Tests/CliDocTests/CliDocTests.swift @@ -0,0 +1,66 @@ +@testable import CliDoc +@preconcurrency import Rainbow +import Testing + +let setupRainbow: Bool = { + Rainbow.enabled = true + Rainbow.outputTarget = .console + return true +}() + +@Test +func checkStringBuilder() { + let group = Group { + Label("foo:") + "bar" + } + + #expect(group.render() == "foo: bar") + + #expect(setupRainbow) + let coloredLabel = group.labelColor(.green) + #expect( + coloredLabel.render() == """ + \("foo:".green) bar + """) +} + +@Test +func checkLabelColorModifier() { + #expect(setupRainbow) + let group = Group(separator: "\n") { + Label("Foo:") + Group(separator: "\n") { + "Bar" + Label("baz:") + Group { + Label("Bang") + "boom" + } + .labelColor(.green) + } + } + .labelColor(.blue) + // .labelColor(.green) + print(type(of: group)) + print(type(of: group.body)) + + let expected = """ + \("Foo:".blue) + Bar + \("baz:".blue) + \("Bang".green) boom + """ + #expect(group.render() == expected) +} + +@Test +func checkNote() { + #expect(setupRainbow) + let note = Note { + "My note..." + } + .labelColor(.yellow) + + #expect(note.render() == "\("NOTE:".yellow) My note...") +}