feat: Moves core functionality into it's own library.
This commit is contained in:
73
Sources/CliDocCore/Builder.swift
Normal file
73
Sources/CliDocCore/Builder.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
@resultBuilder
|
||||
public enum TextBuilder {
|
||||
|
||||
@inlinable
|
||||
public static func buildPartialBlock<N: TextNode>(first: N) -> N {
|
||||
first
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildPartialBlock<N0: TextNode, N1: TextNode>(accumulated: N0, next: N1) -> NodeContainer {
|
||||
.init(nodes: [accumulated, next])
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildArray<N: TextNode>(_ components: [N]) -> NodeContainer {
|
||||
.init(nodes: components)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildBlock<N: TextNode>(_ components: N...) -> NodeContainer {
|
||||
.init(nodes: components)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildEither<N: TextNode, N1: TextNode>(first component: N) -> EitherNode<N, N1> {
|
||||
.first(component)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildEither<N: TextNode, N1: TextNode>(second component: N1) -> EitherNode<N, N1> {
|
||||
.second(component)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public static func buildOptional<N: TextNode>(_ component: N?) -> N? {
|
||||
component
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum EitherNode<N: TextNode, N1: TextNode>: TextNode {
|
||||
case first(N)
|
||||
case second(N1)
|
||||
|
||||
public var body: some TextNode {
|
||||
switch self {
|
||||
case let .first(node): return node.eraseToAnyTextNode()
|
||||
case let .second(node): return node.eraseToAnyTextNode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 var body: some TextNode {
|
||||
nodes.reduce("") { $0 + $1.render() }
|
||||
}
|
||||
}
|
||||
24
Sources/CliDocCore/Modifiers/ColorModifier.swift
Normal file
24
Sources/CliDocCore/Modifiers/ColorModifier.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
import Rainbow
|
||||
|
||||
public extension TextNode {
|
||||
@inlinable
|
||||
func color(_ color: NamedColor) -> some TextNode {
|
||||
modifier(ColorModifier(color: color))
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
struct ColorModifier<Content: TextNode>: NodeModifier {
|
||||
@usableFromInline
|
||||
let color: NamedColor
|
||||
|
||||
@usableFromInline
|
||||
init(color: NamedColor) {
|
||||
self.color = color
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func render(content: Content) -> some TextNode {
|
||||
content.render().applyingColor(color)
|
||||
}
|
||||
}
|
||||
32
Sources/CliDocCore/Modifiers/TextStyleModifier.swift
Normal file
32
Sources/CliDocCore/Modifiers/TextStyleModifier.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
import Rainbow
|
||||
|
||||
public extension TextNode {
|
||||
@inlinable
|
||||
func textStyle(_ styles: Style...) -> some TextNode {
|
||||
modifier(StyleModifier(styles: styles))
|
||||
}
|
||||
|
||||
@inlinable
|
||||
func textStyle(_ styles: [Style]) -> some TextNode {
|
||||
modifier(StyleModifier(styles: styles))
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
struct StyleModifier<Content: TextNode>: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
48
Sources/CliDocCore/NodeModifier.swift
Normal file
48
Sources/CliDocCore/NodeModifier.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
import Rainbow
|
||||
|
||||
public protocol NodeModifier {
|
||||
// swiftlint:disable type_name
|
||||
associatedtype _Body: TextNode
|
||||
typealias Body = _Body
|
||||
// swiftlint:enable type_name
|
||||
|
||||
associatedtype Content
|
||||
|
||||
@TextBuilder
|
||||
func render(content: Content) -> Body
|
||||
}
|
||||
|
||||
public struct ModifiedNode<Content: TextNode, Modifier: NodeModifier> {
|
||||
|
||||
@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 var body: some TextNode {
|
||||
modifier.render(content: content)
|
||||
}
|
||||
}
|
||||
|
||||
extension ModifiedNode: NodeRepresentable where Self: TextNode {
|
||||
@inlinable
|
||||
public func render() -> String {
|
||||
body.render()
|
||||
}
|
||||
}
|
||||
|
||||
public extension TextNode {
|
||||
@inlinable
|
||||
func modifier<M: NodeModifier>(_ modifier: M) -> ModifiedNode<Self, M> {
|
||||
.init(content: self, modifier: modifier)
|
||||
}
|
||||
}
|
||||
21
Sources/CliDocCore/Nodes/AnyTextNode.swift
Normal file
21
Sources/CliDocCore/Nodes/AnyTextNode.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
/// A type-erased text node.
|
||||
public struct AnyTextNode: TextNode {
|
||||
@usableFromInline
|
||||
let makeString: () -> String
|
||||
|
||||
@inlinable
|
||||
public init<N: TextNode>(_ node: N) {
|
||||
self.makeString = node.render
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var body: some TextNode { makeString() }
|
||||
}
|
||||
|
||||
public extension TextNode {
|
||||
|
||||
@inlinable
|
||||
func eraseToAnyTextNode() -> AnyTextNode {
|
||||
AnyTextNode(self)
|
||||
}
|
||||
}
|
||||
11
Sources/CliDocCore/Nodes/Empty.swift
Normal file
11
Sources/CliDocCore/Nodes/Empty.swift
Normal file
@@ -0,0 +1,11 @@
|
||||
/// An empty text node.
|
||||
public struct Empty: TextNode {
|
||||
|
||||
@inlinable
|
||||
public init() {}
|
||||
|
||||
@inlinable
|
||||
public var body: some TextNode {
|
||||
""
|
||||
}
|
||||
}
|
||||
16
Sources/CliDocCore/Nodes/Group.swift
Normal file
16
Sources/CliDocCore/Nodes/Group.swift
Normal file
@@ -0,0 +1,16 @@
|
||||
public struct Group<Content: TextNode>: TextNode {
|
||||
@usableFromInline
|
||||
var content: Content
|
||||
|
||||
@inlinable
|
||||
public init(
|
||||
@TextBuilder content: () -> Content
|
||||
) {
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var body: some TextNode {
|
||||
content
|
||||
}
|
||||
}
|
||||
23
Sources/CliDocCore/Nodes/HStack.swift
Normal file
23
Sources/CliDocCore/Nodes/HStack.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
public struct HStack: TextNode {
|
||||
|
||||
@usableFromInline
|
||||
let content: [any TextNode]
|
||||
|
||||
@usableFromInline
|
||||
let separator: any TextNode
|
||||
|
||||
@inlinable
|
||||
public init(
|
||||
spacing: Int = 1,
|
||||
@TextBuilder content: () -> any TextNode
|
||||
) {
|
||||
self.content = array(from: content())
|
||||
self.separator = seperator(" ", count: spacing > 0 ? spacing - 1 : 0)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var body: some TextNode {
|
||||
content.removingEmptys()
|
||||
.joined(separator: separator.render())
|
||||
}
|
||||
}
|
||||
96
Sources/CliDocCore/Nodes/Section.swift
Normal file
96
Sources/CliDocCore/Nodes/Section.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
// TODO: Add vertical spacing.
|
||||
public struct Section<Header: TextNode, Content: TextNode, Footer: TextNode>: 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<S: SectionStyle>(_ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Sources/CliDocCore/Nodes/VStack.swift
Normal file
23
Sources/CliDocCore/Nodes/VStack.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
public struct VStack: TextNode {
|
||||
|
||||
@usableFromInline
|
||||
let content: [any TextNode]
|
||||
|
||||
@usableFromInline
|
||||
let separator: any TextNode
|
||||
|
||||
@inlinable
|
||||
public init(
|
||||
spacing: Int = 1,
|
||||
@TextBuilder content: () -> any TextNode
|
||||
) {
|
||||
self.content = array(from: content())
|
||||
self.separator = seperator("\n", count: spacing > 0 ? spacing - 1 : 0)
|
||||
}
|
||||
|
||||
@inlinable
|
||||
public var body: some TextNode {
|
||||
content.removingEmptys()
|
||||
.joined(separator: separator.render())
|
||||
}
|
||||
}
|
||||
67
Sources/CliDocCore/TextNode.swift
Normal file
67
Sources/CliDocCore/TextNode.swift
Normal file
@@ -0,0 +1,67 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - String
|
||||
|
||||
extension String: NodeRepresentable {
|
||||
public func render() -> String {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
extension String: TextNode {
|
||||
public var body: some TextNode {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Optional
|
||||
|
||||
extension Optional: TextNode where Wrapped: TextNode {
|
||||
@TextBuilder
|
||||
public var body: some TextNode {
|
||||
switch self {
|
||||
case let .some(node):
|
||||
node
|
||||
case .none:
|
||||
Empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional: NodeRepresentable where Wrapped: NodeRepresentable, Wrapped: TextNode {
|
||||
|
||||
public func render() -> String {
|
||||
body.render()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Array
|
||||
|
||||
extension Array: TextNode where Element: TextNode {
|
||||
public var body: some TextNode {
|
||||
NodeContainer(nodes: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array: NodeRepresentable where Element: NodeRepresentable, Element: TextNode {
|
||||
public func render() -> String {
|
||||
body.render()
|
||||
}
|
||||
}
|
||||
36
Sources/CliDocCore/Utils.swift
Normal file
36
Sources/CliDocCore/Utils.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
@usableFromInline
|
||||
func array(from node: any TextNode) -> [any TextNode] {
|
||||
if let container = node as? NodeContainer {
|
||||
return container.nodes
|
||||
} else if let array = node as? [any TextNode] {
|
||||
return array
|
||||
} else {
|
||||
return [node]
|
||||
}
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func seperator(_ separator: String, count: Int) -> any TextNode {
|
||||
assert(count >= 0, "Invalid count while creating a separator")
|
||||
|
||||
var output = ""
|
||||
for _ in 0 ... count {
|
||||
output += separator
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
extension Array where Element == (any TextNode) {
|
||||
|
||||
@usableFromInline
|
||||
func removingEmptys() -> [String] {
|
||||
compactMap { node in
|
||||
let string = node.render()
|
||||
if string == "" {
|
||||
return nil
|
||||
}
|
||||
return string
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user