From e7bbbec7c2192d8187beca1d43a694b22e85d6f4 Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Thu, 5 Dec 2024 09:59:30 -0500 Subject: [PATCH] feat: Moves core functionality into it's own library. --- Package.swift | 16 +- Sources/CliDoc/BaseNodes/Section.swift | 46 ---- Sources/CliDoc/Exports.swift | 1 + .../CliDoc/Modifiers/LabelStyleModifier.swift | 35 --- .../CliDoc/Modifiers/NoteStyleModifier.swift | 30 --- Sources/CliDoc/Modifiers/SectionStyle.swift | 37 ---- .../CliDoc/Modifiers/ShellCommandStyle.swift | 28 --- Sources/CliDoc/Nodes/ExampleSection.swift | 74 ++++--- Sources/CliDoc/Nodes/Note.swift | 31 +++ Sources/CliDoc/Nodes/ShellCommand.swift | 31 +++ Sources/{CliDoc => CliDocCore}/Builder.swift | 0 .../Modifiers/ColorModifier.swift | 0 .../Modifiers/TextStyleModifier.swift | 0 .../{CliDoc => CliDocCore}/NodeModifier.swift | 24 +- Sources/CliDocCore/Nodes/AnyTextNode.swift | 21 ++ .../Nodes}/Empty.swift | 0 .../Nodes}/Group.swift | 0 .../Nodes}/HStack.swift | 0 Sources/CliDocCore/Nodes/Section.swift | 96 ++++++++ .../Nodes}/VStack.swift | 0 Sources/{CliDoc => CliDocCore}/TextNode.swift | 38 ++-- Sources/{CliDoc => CliDocCore}/Utils.swift | 0 Tests/CliDocCoreTests/CliDocCoreTests.swift | 136 ++++++++++++ Tests/CliDocTests/CliDocTests.swift | 207 ++++++------------ 24 files changed, 464 insertions(+), 387 deletions(-) delete mode 100644 Sources/CliDoc/BaseNodes/Section.swift create mode 100644 Sources/CliDoc/Exports.swift delete mode 100644 Sources/CliDoc/Modifiers/LabelStyleModifier.swift delete mode 100644 Sources/CliDoc/Modifiers/NoteStyleModifier.swift delete mode 100644 Sources/CliDoc/Modifiers/SectionStyle.swift delete mode 100644 Sources/CliDoc/Modifiers/ShellCommandStyle.swift rename Sources/{CliDoc => CliDocCore}/Builder.swift (100%) rename Sources/{CliDoc => CliDocCore}/Modifiers/ColorModifier.swift (100%) rename Sources/{CliDoc => CliDocCore}/Modifiers/TextStyleModifier.swift (100%) rename Sources/{CliDoc => CliDocCore}/NodeModifier.swift (58%) create mode 100644 Sources/CliDocCore/Nodes/AnyTextNode.swift rename Sources/{CliDoc/BaseNodes => CliDocCore/Nodes}/Empty.swift (100%) rename Sources/{CliDoc/BaseNodes => CliDocCore/Nodes}/Group.swift (100%) rename Sources/{CliDoc/BaseNodes => CliDocCore/Nodes}/HStack.swift (100%) create mode 100644 Sources/CliDocCore/Nodes/Section.swift rename Sources/{CliDoc/BaseNodes => CliDocCore/Nodes}/VStack.swift (100%) rename Sources/{CliDoc => CliDocCore}/TextNode.swift (64%) rename Sources/{CliDoc => CliDocCore}/Utils.swift (100%) create mode 100644 Tests/CliDocCoreTests/CliDocCoreTests.swift diff --git a/Package.swift b/Package.swift index c1d08dd..3c00c1d 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,7 @@ import PackageDescription let package = Package( name: "swift-cli-doc", products: [ + .library(name: "CliDocCore", targets: ["CliDocCore"]), .library(name: "CliDoc", targets: ["CliDoc"]) ], dependencies: [ @@ -12,14 +13,25 @@ let package = Package( ], targets: [ .target( - name: "CliDoc", + name: "CliDocCore", dependencies: [ .product(name: "Rainbow", package: "Rainbow") ] ), + .testTarget( + name: "CliDocCoreTests", + dependencies: ["CliDocCore"] + ), + .target( + name: "CliDoc", + dependencies: [ + "CliDocCore", + .product(name: "Rainbow", package: "Rainbow") + ] + ), .testTarget( name: "CliDocTests", - dependencies: ["CliDoc"] + dependencies: ["CliDocCore", "CliDoc"] ) ] ) diff --git a/Sources/CliDoc/BaseNodes/Section.swift b/Sources/CliDoc/BaseNodes/Section.swift deleted file mode 100644 index 243e257..0000000 --- a/Sources/CliDoc/BaseNodes/Section.swift +++ /dev/null @@ -1,46 +0,0 @@ -public struct Section: TextNode { - - @usableFromInline - let header: Header - - @usableFromInline - let content: Content - - @usableFromInline - let footer: Footer - - @inlinable - public init( - @TextBuilder header: () -> Header, - @TextBuilder content: () -> Content, - @TextBuilder footer: () -> Footer - ) { - self.header = header() - self.content = content() - self.footer = footer() - } - - public var body: some TextNode { - style(.default) - } -} - -public extension Section where Footer == Empty { - @inlinable - init( - @TextBuilder header: () -> Header, - @TextBuilder content: () -> Content - ) { - self.init(header: header, content: content) { Empty() } - } -} - -public extension Section where Header == Empty { - @inlinable - init( - @TextBuilder content: () -> Content, - @TextBuilder footer: () -> Footer - ) { - self.init(header: { Empty() }, content: content, footer: footer) - } -} diff --git a/Sources/CliDoc/Exports.swift b/Sources/CliDoc/Exports.swift new file mode 100644 index 0000000..f0f28c5 --- /dev/null +++ b/Sources/CliDoc/Exports.swift @@ -0,0 +1 @@ +@_exported import CliDocCore diff --git a/Sources/CliDoc/Modifiers/LabelStyleModifier.swift b/Sources/CliDoc/Modifiers/LabelStyleModifier.swift deleted file mode 100644 index a8dd61a..0000000 --- a/Sources/CliDoc/Modifiers/LabelStyleModifier.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Rainbow - -public extension TextNode { - func labelStyle(color: NamedColor? = nil, styles: [Style] = []) -> some TextNode where Self == Label { - modifier(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 { - @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: Label) -> some TextNode { - var label: any TextNode = content.content - label = label.textStyle(styles) - if let color { - label = label.color(color) - } - return Label { label.eraseToAnyTextNode() } - } -} diff --git a/Sources/CliDoc/Modifiers/NoteStyleModifier.swift b/Sources/CliDoc/Modifiers/NoteStyleModifier.swift deleted file mode 100644 index 3d76249..0000000 --- a/Sources/CliDoc/Modifiers/NoteStyleModifier.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Rainbow - -public struct NoteStyleConfiguration { - let label: any TextNode - let content: any TextNode -} - -public extension Note { - func noteStyle(_ modifier: S) -> some TextNode { - modifier.render(content: .init(label: label, content: content)) - } -} - -public protocol NoteStyleModifier: NodeModifier where Content == NoteStyleConfiguration {} - -public extension NoteStyleModifier where Self == DefaultNoteStyle { - static var `default`: Self { - DefaultNoteStyle() - } -} - -public struct DefaultNoteStyle: NoteStyleModifier { - - public func render(content: NoteStyleConfiguration) -> some TextNode { - HStack { - content.label.color(.yellow).textStyle(.bold) - content.content - } - } -} diff --git a/Sources/CliDoc/Modifiers/SectionStyle.swift b/Sources/CliDoc/Modifiers/SectionStyle.swift deleted file mode 100644 index a314a16..0000000 --- a/Sources/CliDoc/Modifiers/SectionStyle.swift +++ /dev/null @@ -1,37 +0,0 @@ -public extension Section { - - @inlinable - func style(_ style: S) -> some TextNode { - style.render(content: .init(header: header, content: content, footer: footer)) - } -} - -public struct SectionConfiguration { - public let header: any TextNode - public let content: any TextNode - public let footer: any TextNode - - @usableFromInline - init(header: any TextNode, content: any TextNode, footer: any TextNode) { - self.header = header - self.content = content - self.footer = footer - } -} - -public protocol SectionStyle: NodeModifier where Content == SectionConfiguration {} - -public extension SectionStyle where Self == DefaultSectionStyle { - static var `default`: Self { DefaultSectionStyle() } -} - -public struct DefaultSectionStyle: SectionStyle { - - public func render(content: SectionConfiguration) -> some TextNode { - VStack { - content.header - content.content - content.footer - } - } -} diff --git a/Sources/CliDoc/Modifiers/ShellCommandStyle.swift b/Sources/CliDoc/Modifiers/ShellCommandStyle.swift deleted file mode 100644 index a41ddca..0000000 --- a/Sources/CliDoc/Modifiers/ShellCommandStyle.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Rainbow - -public struct ShellCommandConfiguration { - let symbol: any TextNode - let content: any TextNode -} - -public extension ShellCommand { - func style(_ style: S) -> some TextNode { - style.render(content: .init(symbol: symbol, content: content)) - } -} - -public protocol ShellCommandStyle: NodeModifier where Self.Content == ShellCommandConfiguration {} - -public extension ShellCommandStyle where Self == DefaultShellCommandStyle { - static var `default`: Self { DefaultShellCommandStyle() } -} - -public struct DefaultShellCommandStyle: ShellCommandStyle { - - public func render(content: ShellCommandConfiguration) -> some TextNode { - HStack { - content.symbol - content.content.textStyle(.italic) - } - } -} diff --git a/Sources/CliDoc/Nodes/ExampleSection.swift b/Sources/CliDoc/Nodes/ExampleSection.swift index 2f50f02..a1e7285 100644 --- a/Sources/CliDoc/Nodes/ExampleSection.swift +++ b/Sources/CliDoc/Nodes/ExampleSection.swift @@ -57,25 +57,6 @@ public struct ExampleSectionConfiguration { } } -// MARK: - Style - -public extension ExampleSection { - - func style(_ style: S) -> some TextNode { - style.render(content: configuration) - } - - func exampleStyle(_ style: S) -> some TextNode { - DefaultExamplesStyle(exampleStyle: style).render(content: configuration) - } -} - -extension Array where Element == ExampleSection.Example { - func exampleStyle(_ style: S) -> some TextNode { - style.render(content: .init(examples: self)) - } -} - public struct ExampleConfiguration { @usableFromInline let examples: [ExampleSection.Example] @@ -86,12 +67,42 @@ public struct ExampleConfiguration { } } +// MARK: - Style + public protocol ExampleSectionStyle: NodeModifier where Content == ExampleSectionConfiguration {} public protocol ExampleStyle: NodeModifier where Content == ExampleConfiguration {} -public extension ExampleSectionStyle where Self == DefaultExamplesStyle { - static func `default`(exampleStyle: any ExampleStyle = .default) -> Self { - DefaultExamplesStyle(exampleStyle: exampleStyle) +public extension ExampleSection { + + @inlinable + func style(_ style: S) -> some TextNode { + style.render(content: configuration) + } + + @inlinable + func exampleStyle(_ style: S) -> some TextNode { + DefaultExampleSectionStyle(exampleStyle: style).render(content: configuration) + } +} + +extension Array where Element == ExampleSection.Example { + @inlinable + func exampleStyle(_ style: S) -> some TextNode { + style.render(content: .init(examples: self)) + } +} + +public extension ExampleSectionStyle { + @inlinable + static func `default`(exampleStyle: S) -> DefaultExampleSectionStyle { + DefaultExampleSectionStyle(exampleStyle: exampleStyle) + } +} + +public extension ExampleSectionStyle where Self == DefaultExampleSectionStyle { + @inlinable + static func `default`() -> DefaultExampleSectionStyle { + DefaultExampleSectionStyle() } } @@ -101,19 +112,21 @@ public extension ExampleStyle where Self == DefaultExampleStyle { } } -public struct DefaultExamplesStyle: ExampleSectionStyle { +public struct DefaultExampleSectionStyle: ExampleSectionStyle { @usableFromInline - let exampleStyle: any ExampleStyle + let exampleStyle: Style @inlinable - public init(exampleStyle: any ExampleStyle = .default) { + public init(exampleStyle: Style) { self.exampleStyle = exampleStyle } @inlinable public func render(content: ExampleSectionConfiguration) -> some TextNode { - VStack(spacing: 2) { + Section { + exampleStyle.render(content: .init(examples: content.examples)) + } header: { HStack { content.title .color(.yellow) @@ -122,13 +135,20 @@ public struct DefaultExamplesStyle: ExampleSectionStyle { content.label .textStyle(.italic) } - exampleStyle.render(content: .init(examples: content.examples)) } } } +public extension DefaultExampleSectionStyle where Style == DefaultExampleStyle { + @inlinable + init() { + self.init(exampleStyle: .default) + } +} + public struct DefaultExampleStyle: ExampleStyle { + @inlinable public func render(content: ExampleConfiguration) -> some TextNode { VStack(spacing: 2) { content.examples.map { example in diff --git a/Sources/CliDoc/Nodes/Note.swift b/Sources/CliDoc/Nodes/Note.swift index 01cf69a..f54a328 100644 --- a/Sources/CliDoc/Nodes/Note.swift +++ b/Sources/CliDoc/Nodes/Note.swift @@ -72,3 +72,34 @@ public extension Note where Label == String, Content == String { self.init(label, content: content) } } + +public struct NoteStyleConfiguration { + let label: any TextNode + let content: any TextNode +} + +public extension Note { + func noteStyle(_ modifier: S) -> some TextNode { + modifier.render(content: .init(label: label, content: content)) + } +} + +// MARK: - Style + +public protocol NoteStyleModifier: NodeModifier where Content == NoteStyleConfiguration {} + +public extension NoteStyleModifier where Self == DefaultNoteStyle { + static var `default`: Self { + DefaultNoteStyle() + } +} + +public struct DefaultNoteStyle: NoteStyleModifier { + + public func render(content: NoteStyleConfiguration) -> some TextNode { + HStack { + content.label.color(.yellow).textStyle(.bold) + content.content + } + } +} diff --git a/Sources/CliDoc/Nodes/ShellCommand.swift b/Sources/CliDoc/Nodes/ShellCommand.swift index 5063d01..2a400df 100644 --- a/Sources/CliDoc/Nodes/ShellCommand.swift +++ b/Sources/CliDoc/Nodes/ShellCommand.swift @@ -1,3 +1,5 @@ +import Rainbow + public struct ShellCommand: TextNode { @usableFromInline @@ -30,3 +32,32 @@ public extension ShellCommand where Content == String { self.init(symbol: symbol) { content } } } + +public struct ShellCommandConfiguration { + let symbol: any TextNode + let content: any TextNode +} + +public extension ShellCommand { + func style(_ style: S) -> some TextNode { + style.render(content: .init(symbol: symbol, content: content)) + } +} + +// MARK: - Style + +public protocol ShellCommandStyle: NodeModifier where Self.Content == ShellCommandConfiguration {} + +public extension ShellCommandStyle where Self == DefaultShellCommandStyle { + static var `default`: Self { DefaultShellCommandStyle() } +} + +public struct DefaultShellCommandStyle: ShellCommandStyle { + + public func render(content: ShellCommandConfiguration) -> some TextNode { + HStack { + content.symbol + content.content.textStyle(.italic) + } + } +} diff --git a/Sources/CliDoc/Builder.swift b/Sources/CliDocCore/Builder.swift similarity index 100% rename from Sources/CliDoc/Builder.swift rename to Sources/CliDocCore/Builder.swift diff --git a/Sources/CliDoc/Modifiers/ColorModifier.swift b/Sources/CliDocCore/Modifiers/ColorModifier.swift similarity index 100% rename from Sources/CliDoc/Modifiers/ColorModifier.swift rename to Sources/CliDocCore/Modifiers/ColorModifier.swift diff --git a/Sources/CliDoc/Modifiers/TextStyleModifier.swift b/Sources/CliDocCore/Modifiers/TextStyleModifier.swift similarity index 100% rename from Sources/CliDoc/Modifiers/TextStyleModifier.swift rename to Sources/CliDocCore/Modifiers/TextStyleModifier.swift diff --git a/Sources/CliDoc/NodeModifier.swift b/Sources/CliDocCore/NodeModifier.swift similarity index 58% rename from Sources/CliDoc/NodeModifier.swift rename to Sources/CliDocCore/NodeModifier.swift index 9ed763d..5fccadc 100644 --- a/Sources/CliDoc/NodeModifier.swift +++ b/Sources/CliDocCore/NodeModifier.swift @@ -12,24 +12,8 @@ public protocol NodeModifier { func render(content: Content) -> Body } -public extension NodeModifier { - - func concat(_ modifier: T) -> ConcatModifier { - 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 @@ -47,14 +31,10 @@ extension ModifiedNode: TextNode where Modifier.Content == Content { public var body: some TextNode { modifier.render(content: content) } - - @inlinable - func apply(_ modifier: M) -> ModifiedNode> { - return .init(content: content, modifier: self.modifier.concat(modifier)) - } } extension ModifiedNode: NodeRepresentable where Self: TextNode { + @inlinable public func render() -> String { body.render() } diff --git a/Sources/CliDocCore/Nodes/AnyTextNode.swift b/Sources/CliDocCore/Nodes/AnyTextNode.swift new file mode 100644 index 0000000..b042c7d --- /dev/null +++ b/Sources/CliDocCore/Nodes/AnyTextNode.swift @@ -0,0 +1,21 @@ +/// A type-erased text node. +public struct AnyTextNode: TextNode { + @usableFromInline + let makeString: () -> String + + @inlinable + public init(_ node: N) { + self.makeString = node.render + } + + @inlinable + public var body: some TextNode { makeString() } +} + +public extension TextNode { + + @inlinable + func eraseToAnyTextNode() -> AnyTextNode { + AnyTextNode(self) + } +} diff --git a/Sources/CliDoc/BaseNodes/Empty.swift b/Sources/CliDocCore/Nodes/Empty.swift similarity index 100% rename from Sources/CliDoc/BaseNodes/Empty.swift rename to Sources/CliDocCore/Nodes/Empty.swift diff --git a/Sources/CliDoc/BaseNodes/Group.swift b/Sources/CliDocCore/Nodes/Group.swift similarity index 100% rename from Sources/CliDoc/BaseNodes/Group.swift rename to Sources/CliDocCore/Nodes/Group.swift diff --git a/Sources/CliDoc/BaseNodes/HStack.swift b/Sources/CliDocCore/Nodes/HStack.swift similarity index 100% rename from Sources/CliDoc/BaseNodes/HStack.swift rename to Sources/CliDocCore/Nodes/HStack.swift diff --git a/Sources/CliDocCore/Nodes/Section.swift b/Sources/CliDocCore/Nodes/Section.swift new file mode 100644 index 0000000..6e7378d --- /dev/null +++ b/Sources/CliDocCore/Nodes/Section.swift @@ -0,0 +1,96 @@ +// TODO: Add vertical spacing. +public struct Section: TextNode { + + @usableFromInline + let header: Header + + @usableFromInline + let content: Content + + @usableFromInline + let footer: Footer + + @inlinable + public init( + @TextBuilder content: () -> Content, + @TextBuilder header: () -> Header, + @TextBuilder footer: () -> Footer + ) { + self.header = header() + self.content = content() + self.footer = footer() + } + + public var body: some TextNode { + style(.default) + } +} + +public extension Section where Footer == Empty { + @inlinable + init( + @TextBuilder content: () -> Content, + @TextBuilder header: () -> Header + ) { + self.init(content: content, header: header) { Empty() } + } +} + +public extension Section where Header == Empty { + @inlinable + init( + @TextBuilder content: () -> Content, + @TextBuilder footer: () -> Footer + ) { + self.init(content: content, header: { Empty() }, footer: footer) + } +} + +public extension Section where Header == Empty, Footer == Empty { + @inlinable + init( + @TextBuilder content: () -> Content + ) { + self.init(content: content, header: { Empty() }, footer: { Empty() }) + } +} + +// MARK: - Style + +public extension Section { + + @inlinable + func style(_ style: S) -> some TextNode { + style.render(content: .init(header: header, content: content, footer: footer)) + } +} + +public struct SectionConfiguration { + public let header: any TextNode + public let content: any TextNode + public let footer: any TextNode + + @usableFromInline + init(header: any TextNode, content: any TextNode, footer: any TextNode) { + self.header = header + self.content = content + self.footer = footer + } +} + +public protocol SectionStyle: NodeModifier where Content == SectionConfiguration {} + +public extension SectionStyle where Self == DefaultSectionStyle { + static var `default`: Self { DefaultSectionStyle() } +} + +public struct DefaultSectionStyle: SectionStyle { + + public func render(content: SectionConfiguration) -> some TextNode { + VStack(spacing: 2) { + content.header + content.content + content.footer + } + } +} diff --git a/Sources/CliDoc/BaseNodes/VStack.swift b/Sources/CliDocCore/Nodes/VStack.swift similarity index 100% rename from Sources/CliDoc/BaseNodes/VStack.swift rename to Sources/CliDocCore/Nodes/VStack.swift diff --git a/Sources/CliDoc/TextNode.swift b/Sources/CliDocCore/TextNode.swift similarity index 64% rename from Sources/CliDoc/TextNode.swift rename to Sources/CliDocCore/TextNode.swift index 919d531..fd3ffce 100644 --- a/Sources/CliDoc/TextNode.swift +++ b/Sources/CliDocCore/TextNode.swift @@ -17,6 +17,8 @@ public extension TextNode { } } +// MARK: - String + extension String: NodeRepresentable { public func render() -> String { self @@ -29,45 +31,37 @@ extension String: TextNode { } } -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) - } -} +// MARK: - Optional extension Optional: TextNode where Wrapped: TextNode { + @TextBuilder public var body: some TextNode { - guard let node = self else { return "".eraseToAnyTextNode() } - return node.eraseToAnyTextNode() + switch self { + case let .some(node): + node + case .none: + Empty() + } } } -extension Optional: NodeRepresentable where Wrapped: NodeRepresentable { +extension Optional: NodeRepresentable where Wrapped: NodeRepresentable, Wrapped: TextNode { public func render() -> String { - guard let node = self else { return "" } - return node.render() + body.render() } } +// MARK: - Array + extension Array: TextNode where Element: TextNode { public var body: some TextNode { NodeContainer(nodes: self) } } -extension Array: NodeRepresentable where Element: NodeRepresentable { +extension Array: NodeRepresentable where Element: NodeRepresentable, Element: TextNode { public func render() -> String { - map { $0.render() }.joined() + body.render() } } diff --git a/Sources/CliDoc/Utils.swift b/Sources/CliDocCore/Utils.swift similarity index 100% rename from Sources/CliDoc/Utils.swift rename to Sources/CliDocCore/Utils.swift diff --git a/Tests/CliDocCoreTests/CliDocCoreTests.swift b/Tests/CliDocCoreTests/CliDocCoreTests.swift new file mode 100644 index 0000000..031522c --- /dev/null +++ b/Tests/CliDocCoreTests/CliDocCoreTests.swift @@ -0,0 +1,136 @@ +@testable import CliDocCore +@preconcurrency import Rainbow +import Testing + +@Suite("CliDocCore Tests") +struct CliDocCoreTests { + // Ensure that rainbow is setup, for test comparisons to work properly. + let setupRainbow: Bool = { + Rainbow.enabled = true + Rainbow.outputTarget = .console + return true + }() + + @Test + func testGroup() { + #expect(setupRainbow) + let group = Group { + "foo" + "bar" + if setupRainbow { + "baz".color(.blue) + } + } + #expect(group.render() == "foobar\("baz".blue)") + } + + @Test + func testHStack() { + #expect(setupRainbow) + let stack = HStack { + "foo" + "bar" + } + #expect(stack.render() == "foo bar") + } + + @Test + func testVStack() { + #expect(setupRainbow) + let stack = VStack { + "foo" + "bar" + } + #expect(stack.render() == """ + foo + bar + """) + } + + @Test + func testOptionalTextNode() { + let someNode = String?.some("string") + #expect(someNode.body.render() == "string") + #expect(someNode.render() == "string") + + let noneNode = String?.none + #expect(noneNode.body.render() == "") + #expect(noneNode.render() == "") + } + + @Test + func testArrayTextNode() { + let array = ["foo", " ", "bar"] + #expect(array.body.render() == "foo bar") + #expect(array.render() == "foo bar") + } + + @Test( + arguments: SectionArg.arguments + ) + func testSection(arg: SectionArg) { + #expect(setupRainbow) + printIfNotEqual(arg.section.render(), arg.expected) + #expect(arg.section.render() == arg.expected) + } + + struct SectionArg: @unchecked Sendable { + let section: any TextNode + let expected: String + + static var arguments: [Self] { + [ + .init( + section: Section { + "Content" + } header: { + "Header" + } footer: { + "Footer" + }, + expected: """ + Header + + Content + + Footer + """ + ), + .init( + section: Section { + "Content" + } footer: { + "Footer" + }, + expected: """ + Content + + Footer + """ + ), + .init( + section: Section { + "Content" + } header: { + "Header" + }, + expected: """ + Header + + Content + """ + ) + ] + } + } +} + +@discardableResult +func printIfNotEqual(_ lhs: String, _ rhs: String) -> Bool { + guard lhs == rhs else { + print(lhs) + print(rhs) + return false + } + return true +} diff --git a/Tests/CliDocTests/CliDocTests.swift b/Tests/CliDocTests/CliDocTests.swift index 71e2fe1..dd2cbea 100644 --- a/Tests/CliDocTests/CliDocTests.swift +++ b/Tests/CliDocTests/CliDocTests.swift @@ -1,154 +1,79 @@ @testable import CliDoc +@testable import CliDocCore @preconcurrency import Rainbow import Testing -// Ensure that rainbow is setup, for test comparisons to work properly. -let setupRainbow: Bool = { - Rainbow.enabled = true - Rainbow.outputTarget = .console - return true -}() +@Suite("CliDoc tests") +struct CliDocTests { + // Ensure that rainbow is setup, for test comparisons to work properly. + let setupRainbow: Bool = { + Rainbow.enabled = true + Rainbow.outputTarget = .console + return true + }() -@Test -func testGroup() { - #expect(setupRainbow) - let group = Group { - "foo" - "bar" + @Test + func testNote() { + #expect(setupRainbow) + let note = Note(content: "Some note.") + let expected = """ + \("NOTE:".yellow.bold) Some note. + """ + #expect(note.render() == expected) } - #expect(group.render() == "foobar") -} -@Test -func testHStack() { - #expect(setupRainbow) - let stack = HStack { - "foo" - "bar" + @Test + func testExamples() { + #expect(setupRainbow) + let examples = ExampleSection( + "Examples:", + label: "Some common usage examples.", + examples: [("First", "ls -lah"), ("Second", "find . -name foo")] + ) + + let expected = """ + \("Examples:".yellow.bold)\(" ")\("Some common usage examples.".italic) + + \("First".green.bold) + $ \("ls -lah".italic) + + \("Second".green.bold) + $ \("find . -name foo".italic) + """ + let result = printIfNotEqual(examples.render(), expected) + #expect(result) } - #expect(stack.render() == "foo bar") -} -@Test -func testVStack() { - #expect(setupRainbow) - let stack = VStack { - "foo" - "bar" - } - #expect(stack.render() == """ - foo - bar - """) -} + @Test + func testExamplesWithCustomExampleOnlyStyle() { + #expect(setupRainbow) + let examples = ExampleSection( + "Examples:", + label: "Some common usage examples.", + examples: [("First", "ls -lah"), ("Second", "find . -name foo")] + ) + // .exampleStyle(CustomExampleOnlyStyle()) -@Test -func testNote() { - #expect(setupRainbow) - let note = Note(content: "Some note.") - let expected = """ - \("NOTE:".yellow.bold) Some note. - """ - #expect(note.render() == expected) -} + let expected = """ + \("Examples:".applyingStyle(.bold).applyingColor(.yellow)) \("Some common usage examples.".italic) -@Test -func testExamples() { - #expect(setupRainbow) - let examples = ExampleSection( - "Examples:", - label: "Some common usage examples.", - examples: [("First", "ls -lah"), ("Second", "find . -name foo")] - ) + \("First".red) + $ \("ls -lah".italic) - let expected = """ - \("Examples:".yellow.bold)\(" ")\("Some common usage examples.".italic) + \("Second".red) + $ \("find . -name foo".italic) + """ + let result = printIfNotEqual( + examples.exampleStyle(CustomExampleOnlyStyle()).render(), + expected + ) + #expect(result) - \("First".green.bold) - $ \("ls -lah".italic) - - \("Second".green.bold) - $ \("find . -name foo".italic) - """ - let result = printIfNotEqual(examples.render(), expected) - #expect(result) -} - -@Test -func testExamplesWithCustomExampleOnlyStyle() { - #expect(setupRainbow) - let examples = ExampleSection( - "Examples:", - label: "Some common usage examples.", - examples: [("First", "ls -lah"), ("Second", "find . -name foo")] - ) - .exampleStyle(CustomExampleOnlyStyle()) - - let expected = """ - \("Examples:".applyingStyle(.bold).applyingColor(.yellow)) \("Some common usage examples.".italic) - - \("First".red) - $ \("ls -lah".italic) - - \("Second".red) - $ \("find . -name foo".italic) - """ - let result = printIfNotEqual(examples.render(), expected) - #expect(result) -} - -@Test( - arguments: SectionArg.arguments -) -func testSection(arg: SectionArg) { - #expect(setupRainbow) - printIfNotEqual(arg.section.render(), arg.expected) - #expect(arg.section.render() == arg.expected) -} - -struct SectionArg: @unchecked Sendable { - let section: any TextNode - let expected: String - - static var arguments: [Self] { - [ - .init( - section: Section { - "Header" - } content: { - "Content" - } footer: { - "Footer" - }, - expected: """ - Header - Content - Footer - """ - ), - .init( - section: Section { - "Content" - } footer: { - "Footer" - }, - expected: """ - Content - Footer - """ - ), - .init( - section: Section { - "Header" - } content: { - "Content" - }, - expected: """ - Header - Content - """ - ) - ] + let result2 = printIfNotEqual( + examples.style(.custom).render(), + expected + ) + #expect(result2) } } @@ -162,6 +87,12 @@ func printIfNotEqual(_ lhs: String, _ rhs: String) -> Bool { return true } +extension ExampleSectionStyle where Self == DefaultExampleSectionStyle { + static var custom: Self { + .default(exampleStyle: CustomExampleOnlyStyle()) + } +} + struct CustomExampleOnlyStyle: ExampleStyle { func render(content: ExampleConfiguration) -> some TextNode { VStack(spacing: 2) {