diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..8c96693 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +**/*.docc diff --git a/Sources/CliDocCore/Builder.swift b/Sources/CliDocCore/Builder.swift index 3039cfd..40bbb46 100644 --- a/Sources/CliDocCore/Builder.swift +++ b/Sources/CliDocCore/Builder.swift @@ -1,3 +1,6 @@ +/// A result builder for creating ``TextNode`` types, similar to how +/// `ViewBuilder` works in `SwiftUI`. +/// @resultBuilder public enum TextBuilder { @@ -7,27 +10,27 @@ public enum TextBuilder { } @inlinable - public static func buildPartialBlock(accumulated: N0, next: N1) -> NodeContainer { + public static func buildPartialBlock(accumulated: N0, next: N1) -> _NodeContainer { .init(nodes: [accumulated, next]) } @inlinable - public static func buildArray(_ components: [N]) -> NodeContainer { + public static func buildArray(_ components: [N]) -> _NodeContainer { .init(nodes: components) } @inlinable - public static func buildBlock(_ components: N...) -> NodeContainer { + public static func buildBlock(_ components: N...) -> _NodeContainer { .init(nodes: components) } @inlinable - public static func buildEither(first component: N) -> EitherNode { + public static func buildEither(first component: N) -> _EitherNode { .first(component) } @inlinable - public static func buildEither(second component: N1) -> EitherNode { + public static func buildEither(second component: N1) -> _EitherNode { .second(component) } @@ -38,7 +41,8 @@ public enum TextBuilder { } -public enum EitherNode: TextNode { +// swiftlint:disable type_name +public enum _EitherNode: TextNode { case first(N) case second(N1) @@ -50,7 +54,7 @@ public enum EitherNode: TextNode { } } -public struct NodeContainer: TextNode { +public struct _NodeContainer: TextNode { @usableFromInline var nodes: [any TextNode] @@ -58,7 +62,7 @@ public struct NodeContainer: TextNode { @usableFromInline init(nodes: [any TextNode]) { self.nodes = nodes.reduce(into: [any TextNode]()) { array, next in - if let many = next as? NodeContainer { + if let many = next as? _NodeContainer { array += many.nodes } else { array.append(next) @@ -71,3 +75,5 @@ public struct NodeContainer: TextNode { nodes.reduce("") { $0 + $1.render() } } } + +// swiftlint:enable type_name diff --git a/Sources/CliDocCore/Documentation.docc/CliDocCore.md b/Sources/CliDocCore/Documentation.docc/CliDocCore.md new file mode 100644 index 0000000..70c69ab --- /dev/null +++ b/Sources/CliDocCore/Documentation.docc/CliDocCore.md @@ -0,0 +1,30 @@ +# ``CliDocCore`` + +A framework for writing `cli` documentation in a way similar to `SwiftUI`, where +your types conform to ``TextNode`` and implement a ``TextNode/body`` that +returns a ``TextNode``, generally using the essential types described below. + +## Topics + +### Essentials + +- ``VStack`` +- ``HStack`` +- ``Section`` +- ``Group`` +- ``Empty`` +- ``AnyTextNode`` + +### Styling + +- ``SectionStyle`` +- ``DefaultSectionStyle`` +- ``SectionConfiguration`` +- ``TextStyleConfiguration`` + +### Base Protocols + +- ``TextNode`` +- ``TextNodeRepresentable`` +- ``TextStyle`` +- ``TextModifier`` diff --git a/Sources/CliDocCore/Documentation.docc/Resources/section.png b/Sources/CliDocCore/Documentation.docc/Resources/section.png new file mode 100644 index 0000000..631346e Binary files /dev/null and b/Sources/CliDocCore/Documentation.docc/Resources/section.png differ diff --git a/Sources/CliDocCore/Nodes/Section.swift b/Sources/CliDocCore/Nodes/Section.swift index dc9ae82..fb09668 100644 --- a/Sources/CliDocCore/Nodes/Section.swift +++ b/Sources/CliDocCore/Nodes/Section.swift @@ -37,16 +37,11 @@ /// /// print(mySection.render()) /// ``` -/// **Note:** colored output / styling only shows in the terminal. /// -/// ```bash +/// _Below is an image of the output from the `mySection.render()` above._ /// -/// Awesome +/// ![Custom section style image](section.png) /// -/// My super awesome section -/// -/// Note: this is super awesome -/// ``` public struct Section: TextNode { @usableFromInline @@ -124,7 +119,8 @@ public extension Section { } } -/// Holds the type-erased values of a ``Section``, used to style a section. +/// Holds the type-erased values of a ``Section``, that can be used to create +/// custom styling for a section. public struct SectionConfiguration { /// The type-erased header of a section. public let header: any TextNode @@ -143,6 +139,9 @@ public struct SectionConfiguration { } } +/// Used to declare a custom style for a ``Section``. Your custom section style +/// must conform to this protocol. +/// public protocol SectionStyle: TextModifier where Content == SectionConfiguration {} public extension SectionStyle where Self == DefaultSectionStyle { diff --git a/Sources/CliDocCore/Nodes/Separator.swift b/Sources/CliDocCore/Nodes/Separator.swift index 4e7e9b6..b6fea09 100644 --- a/Sources/CliDocCore/Nodes/Separator.swift +++ b/Sources/CliDocCore/Nodes/Separator.swift @@ -1,7 +1,23 @@ +/// A namespace for separator types that are used in text nodes. +/// +/// These are generally used in the ``HStack``'s and ``VStack``'s to +/// specifiy how the nodes they contain are separated. +/// +/// +/// **Note:** +/// +/// > By default nodes do not contain any separators, unless they are written inline. +/// > However, both ``HStack`` and ``VStack`` allow you to specify the separator to use +/// > to control how the individual nodes they contain are separated. +/// public enum Separator { /// Represents a horizontal separator that can be used between text nodes, typically inside /// an ``HStack`` + /// + /// **Note:** + /// > By default nodes do not contain any separators, unless they are written inline. + /// public enum Horizontal: TextNode { /// Separate nodes by spaces of the given count. case space(count: Int = 1) @@ -10,6 +26,12 @@ public enum Separator { case tab(count: Int = 1) /// Separate nodes by the provided string of the given count. + /// + /// **Note:** + /// + /// > This can allow for non-sensical separators, so should only be used + /// > if the provided horizontal separators do not work for you. + /// case custom(String, count: Int = 1) @TextBuilder @@ -28,8 +50,34 @@ public enum Separator { /// Represents a vertical separator that can be used between text nodes, typically inside /// a ``VStack`` + /// + /// **Note:** + /// > By default nodes do not contain any separators, so if you would + /// > like a blank line separating nodes, then a count of `2` is required. + /// > A count of `1` will place nodes on the next line. + /// public enum Vertical: TextNode { + /// Separate nodes by new line characters of the given count. + /// + /// **Note:** + /// > By default nodes do not contain any separtors, so if you would + /// > like a blank line separating nodes, then a count of `2` is required. + /// > A count of `1` will place nodes on the next line. + /// + /// + /// - Parameters: + /// - count: The count of the new lines to use. case newLine(count: Int = 1) + + /// Separate nodes by the supplied string with the given count. + /// + /// **Note:** + /// + /// > This can allow for non-sensical separators, so should only be used + /// > if the provided vertical separators do not work for you. + /// + /// - Parameters: + /// - count: The count of the new lines to use. case custom(String, count: Int = 1) @TextBuilder diff --git a/Sources/CliDocCore/TextModifier.swift b/Sources/CliDocCore/TextModifier.swift index 46a9e1c..5d413a3 100644 --- a/Sources/CliDocCore/TextModifier.swift +++ b/Sources/CliDocCore/TextModifier.swift @@ -1,3 +1,7 @@ +/// A type that can modify a text node before it is rendered. +/// +/// This allows you to create custom styles for your text-nodes. +/// public protocol TextModifier { // swiftlint:disable type_name associatedtype _Body: TextNode @@ -6,6 +10,10 @@ public protocol TextModifier { associatedtype Content + /// Apply custom styling to the text node. + /// + /// - Parameters: + /// - content: The text node to be styled. @TextBuilder func render(content: Content) -> Body } diff --git a/Sources/CliDocCore/TextNode.swift b/Sources/CliDocCore/TextNode.swift index fd3ffce..0cdcb02 100644 --- a/Sources/CliDocCore/TextNode.swift +++ b/Sources/CliDocCore/TextNode.swift @@ -1,8 +1,14 @@ -public protocol NodeRepresentable { +/// A type that can produce a string to be used as a documentation +/// text node. +public protocol TextNodeRepresentable { + + /// Produces the string output to use as the documentation string. func render() -> String } -public protocol TextNode: NodeRepresentable { +/// A type that can produce a string to be used as a documentation +/// text node. +public protocol TextNode: TextNodeRepresentable { // swiftlint:disable type_name associatedtype _Body: TextNode typealias Body = _Body @@ -19,16 +25,12 @@ public extension TextNode { // MARK: - String -extension String: NodeRepresentable { - public func render() -> String { - self - } +extension String: TextNodeRepresentable { + public func render() -> String { self } } extension String: TextNode { - public var body: some TextNode { - self - } + public var body: some TextNode { self } } // MARK: - Optional @@ -45,7 +47,7 @@ extension Optional: TextNode where Wrapped: TextNode { } } -extension Optional: NodeRepresentable where Wrapped: NodeRepresentable, Wrapped: TextNode { +extension Optional: TextNodeRepresentable where Wrapped: TextNodeRepresentable, Wrapped: TextNode { public func render() -> String { body.render() @@ -56,11 +58,11 @@ extension Optional: NodeRepresentable where Wrapped: NodeRepresentable, Wrapped: extension Array: TextNode where Element: TextNode { public var body: some TextNode { - NodeContainer(nodes: self) + _NodeContainer(nodes: self) } } -extension Array: NodeRepresentable where Element: NodeRepresentable, Element: TextNode { +extension Array: TextNodeRepresentable where Element: TextNodeRepresentable, Element: TextNode { public func render() -> String { body.render() } diff --git a/Sources/CliDocCore/TextStyle.swift b/Sources/CliDocCore/TextStyle.swift index 3f8644a..06e837e 100644 --- a/Sources/CliDocCore/TextStyle.swift +++ b/Sources/CliDocCore/TextStyle.swift @@ -2,36 +2,68 @@ import Rainbow public extension TextNode { + /// Apply coloring to a text node. + /// + /// - Parameters: + /// - color: The color to apply to the text node. @inlinable func color(_ color: NamedColor) -> some TextNode { - textStyle(.color(color)) + textStyle(_ColorTextStyle(.foreground(.named(color)))) } + /// Apply coloring to a text node. + /// + /// - Parameters: + /// - bit8: The color to apply to the text node. @inlinable func color(_ bit8: UInt8) -> some TextNode { - textStyle(.color(bit8: bit8)) + textStyle(_ColorTextStyle(.foreground(.bit8(bit8)))) } + /// Apply coloring to a text node using RGB. + /// + /// - Parameters: + /// - red: The red color to apply to the text node. + /// - green: The green color to apply to the text node. + /// - blue: The blue color to apply to the text node. @inlinable func color(_ red: UInt8, _ green: UInt8, _ blue: UInt8) -> some TextNode { - textStyle(.color(rgb: (red, green, blue))) + textStyle(_ColorTextStyle(.foreground(.bit24((red, green, blue))))) } + /// Apply background coloring to a text node. + /// + /// - Parameters: + /// - color: The color to apply to the text node. @inlinable - func backgroundColor(_ name: NamedBackgroundColor) -> some TextNode { - textStyle(.backgroundColor(name)) + func backgroundColor(_ color: NamedBackgroundColor) -> some TextNode { + textStyle(_ColorTextStyle(.background(.named(color)))) } + /// Apply background coloring to a text node. + /// + /// - Parameters: + /// - bit8: The color to apply to the text node. @inlinable func backgroundColor(_ bit8: UInt8) -> some TextNode { - textStyle(.backgroundColor(bit8: bit8)) + textStyle(_ColorTextStyle(.background(.bit8(bit8)))) } + /// Apply background coloring to a text node using RGB. + /// + /// - Parameters: + /// - red: The red color to apply to the text node. + /// - green: The green color to apply to the text node. + /// - blue: The blue color to apply to the text node. @inlinable func backgroundColor(_ red: UInt8, _ green: UInt8, _ blue: UInt8) -> some TextNode { - textStyle(.backgroundColor(rgb: (red, green, blue))) + textStyle(_ColorTextStyle(.background(.bit24((red, green, blue))))) } + /// Apply styles to a text node. + /// + /// - Parameters: + /// - styles: The styles to apply. @inlinable func textStyle(_ styles: S...) -> some TextNode { styles.reduce(render()) { string, style in @@ -39,28 +71,49 @@ public extension TextNode { } } + /// Apply a bold text style to a text node. @inlinable func bold() -> some TextNode { textStyle(.bold) } + /// Apply a dim text style to a text node. @inlinable func dim() -> some TextNode { textStyle(.dim) } + /// Apply an italic text style to a text node. @inlinable func italic() -> some TextNode { textStyle(.italic) } + /// Apply an underline text style to a text node. @inlinable func underline() -> some TextNode { textStyle(.underline) } + /// Apply a blink text style to a text node. @inlinable func blink() -> some TextNode { textStyle(.blink) } + /// Apply a strike-through text style to a text node. @inlinable func strikeThrough() -> some TextNode { textStyle(.strikeThrough) } } +/// A general purpose way of styling a text node. +/// +/// This is generally used for applying styling that can work with any text node. +/// +/// Most of the time you will want to customize styles for a text node's type instead +/// of using this. public protocol TextStyle: TextModifier where Content == TextStyleConfiguration {} +/// A type-erased text node that can be used for creating +/// custom styles. +/// +/// This is generally used to change the style of text nodes as +/// a whole, such as applying text colors or styling such as `bold`. +/// +/// Most of the time you will want to customize styles for a text node's +/// type, instead of using this. +/// public struct TextStyleConfiguration { public let node: any TextNode @@ -70,7 +123,7 @@ public struct TextStyleConfiguration { } } -public extension TextStyle where Self == StyledText { +public extension TextStyle where Self == _StyledText { @inlinable static var bold: Self { .init(.bold) } @@ -91,40 +144,8 @@ public extension TextStyle where Self == StyledText { static var strikeThrough: Self { .init(.strikethrough) } } -public extension TextStyle where Self == ColorTextStyle { - - @inlinable - static func color(_ name: NamedColor) -> Self { - .init(.foreground(.named(name))) - } - - @inlinable - static func color(bit8: UInt8) -> Self { - .init(.foreground(.bit8(bit8))) - } - - @inlinable - static func color(rgb: RGB) -> Self { - .init(.foreground(.bit24(rgb))) - } - - @inlinable - static func backgroundColor(_ name: NamedBackgroundColor) -> Self { - .init(.background(.named(name))) - } - - @inlinable - static func backgroundColor(bit8: UInt8) -> Self { - .init(.background(.bit8(bit8))) - } - - @inlinable - static func backgroundColor(rgb: RGB) -> Self { - .init(.background(.bit24(rgb))) - } -} - -public struct ColorTextStyle: TextStyle { +// swiftlint:disable type_name +public struct _ColorTextStyle: TextStyle { @usableFromInline enum Style { case foreground(ColorType) @@ -150,7 +171,7 @@ public struct ColorTextStyle: TextStyle { } } -public struct StyledText: TextStyle { +public struct _StyledText: TextStyle { @usableFromInline let style: Style @@ -164,3 +185,5 @@ public struct StyledText: TextStyle { content.node.render().applyingStyle(style) } } + +// swiftlint:enable type_name diff --git a/Sources/CliDocCore/Utils.swift b/Sources/CliDocCore/Utils.swift index 183aa9b..9ec4384 100644 --- a/Sources/CliDocCore/Utils.swift +++ b/Sources/CliDocCore/Utils.swift @@ -1,6 +1,6 @@ @usableFromInline func array(from node: any TextNode) -> [any TextNode] { - if let container = node as? NodeContainer { + if let container = node as? _NodeContainer { return container.nodes } else if let array = node as? [any TextNode] { return array diff --git a/justfile b/justfile index 8f93a6e..b7ecd9e 100644 --- a/justfile +++ b/justfile @@ -4,4 +4,5 @@ preview-documentation target="CliDoc": --disable-sandbox \ preview-documentation \ --target {{target}} \ - --include-extended-types + --include-extended-types \ + --enable-inherited-docs