feat: Adds vertical and horizontal separators, renames modifier protocol.

This commit is contained in:
2024-12-05 22:38:42 -05:00
parent bef91c5277
commit 9985b55f88
15 changed files with 253 additions and 93 deletions

View File

@@ -69,8 +69,8 @@ public struct ExampleConfiguration {
// MARK: - Style
public protocol ExampleSectionStyle: NodeModifier where Content == ExampleSectionConfiguration {}
public protocol ExampleStyle: NodeModifier where Content == ExampleConfiguration {}
public protocol ExampleSectionStyle: TextModifier where Content == ExampleSectionConfiguration {}
public protocol ExampleStyle: TextModifier where Content == ExampleConfiguration {}
public extension ExampleSection {
@@ -149,7 +149,7 @@ public struct DefaultExampleStyle: ExampleStyle {
@inlinable
public func render(content: ExampleConfiguration) -> some TextNode {
VStack(spacing: 2) {
VStack(separator: .newLine(count: 2)) {
content.examples.map { example in
VStack {
Label(example.label.green.bold)

View File

@@ -86,7 +86,7 @@ public extension Note {
// MARK: - Style
public protocol NoteStyleModifier: NodeModifier where Content == NoteStyleConfiguration {}
public protocol NoteStyleModifier: TextModifier where Content == NoteStyleConfiguration {}
public extension NoteStyleModifier where Self == DefaultNoteStyle {
static var `default`: Self {

View File

@@ -46,7 +46,7 @@ public extension ShellCommand {
// MARK: - Style
public protocol ShellCommandStyle: NodeModifier where Self.Content == ShellCommandConfiguration {}
public protocol ShellCommandStyle: TextModifier where Self.Content == ShellCommandConfiguration {}
public extension ShellCommandStyle where Self == DefaultShellCommandStyle {
static var `default`: Self { DefaultShellCommandStyle() }

View File

@@ -1,48 +0,0 @@
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)
}
}

View File

@@ -1,4 +1,6 @@
/// An empty text node.
///
/// This gets removed from any output when rendering text nodes.
public struct Empty: TextNode {
@inlinable

View File

@@ -1,4 +1,9 @@
/// A group of text nodes.
///
/// This allows you to group content together, which can optionally be
/// styled.
public struct Group<Content: TextNode>: TextNode {
@usableFromInline
var content: Content

View File

@@ -1,18 +1,19 @@
/// A horizontal group of text nodes.
public struct HStack: TextNode {
@usableFromInline
let content: [any TextNode]
@usableFromInline
let separator: any TextNode
let separator: Separator.Horizontal
@inlinable
public init(
spacing: Int = 1,
separator: Separator.Horizontal = .space(count: 1),
@TextBuilder content: () -> any TextNode
) {
self.content = array(from: content())
self.separator = seperator(" ", count: spacing > 0 ? spacing - 1 : 0)
self.separator = separator
}
@inlinable

View File

@@ -78,7 +78,7 @@ public struct SectionConfiguration {
}
}
public protocol SectionStyle: NodeModifier where Content == SectionConfiguration {}
public protocol SectionStyle: TextModifier where Content == SectionConfiguration {}
public extension SectionStyle where Self == DefaultSectionStyle {
static var `default`: Self { DefaultSectionStyle() }
@@ -87,7 +87,7 @@ public extension SectionStyle where Self == DefaultSectionStyle {
public struct DefaultSectionStyle: SectionStyle {
public func render(content: SectionConfiguration) -> some TextNode {
VStack(spacing: 2) {
VStack(separator: .newLine(count: 2)) {
content.header
content.content
content.footer

View File

@@ -0,0 +1,66 @@
public enum Separator {
/// Represents a horizontal separator that can be used between text nodes, typically inside
/// an ``HStack``
public enum Horizontal: TextNode {
/// Separate nodes by spaces of the given count.
case space(count: Int = 1)
/// Separate nodes by tabs of the given count.
case tab(count: Int = 1)
/// Separate nodes by the provided string of the given count.
case custom(String, count: Int = 1)
@TextBuilder
@inlinable
public var body: some TextNode {
switch self {
case let .tab(count: count):
seperator("\t", count: count)
case let .space(count: count):
seperator(" ", count: count)
case let .custom(string, count: count):
seperator(string, count: count)
}
}
}
/// Represents a vertical separator that can be used between text nodes, typically inside
/// a ``VStack``
public enum Vertical: TextNode {
case newLine(count: Int = 1)
case custom(String, count: Int = 1)
@TextBuilder
@inlinable
public var body: some TextNode {
switch self {
case let .newLine(count: count):
seperator("\n", count: count)
case let .custom(string, count: count):
seperator(string, count: count)
}
}
}
}
@usableFromInline
func ensuredCount(_ count: Int) -> Int {
guard count >= 1 else { return 1 }
return count
}
@usableFromInline
func seperator(_ separator: String, count: Int) -> some TextNode {
let count = ensuredCount(count)
assert(count >= 1, "Invalid count while creating a separator")
var output = ""
for _ in 1 ... count {
output += separator
}
return output
}

View File

@@ -1,18 +1,21 @@
public struct VStack: TextNode {
/// A vertical stack of text nodes.
///
///
public struct VStack: TextNode {
@usableFromInline
let content: [any TextNode]
@usableFromInline
let separator: any TextNode
let separator: Separator.Vertical
@inlinable
public init(
spacing: Int = 1,
separator: Separator.Vertical = .newLine(count: 1),
@TextBuilder content: () -> any TextNode
) {
self.content = array(from: content())
self.separator = seperator("\n", count: spacing > 0 ? spacing - 1 : 0)
self.separator = separator
}
@inlinable

View File

@@ -0,0 +1,11 @@
public protocol TextModifier {
// swiftlint:disable type_name
associatedtype _Body: TextNode
typealias Body = _Body
// swiftlint:enable type_name
associatedtype Content
@TextBuilder
func render(content: Content) -> Body
}

View File

@@ -13,14 +13,29 @@ public extension TextNode {
}
@inlinable
func color(red: UInt8, green: UInt8, blue: UInt8) -> some TextNode {
func color(_ red: UInt8, _ green: UInt8, _ blue: UInt8) -> some TextNode {
textStyle(.color(rgb: (red, green, blue)))
}
@inlinable
func backgroundColor(_ name: NamedBackgroundColor) -> some TextNode {
textStyle(.backgroundColor(name))
}
@inlinable
func backgroundColor(_ bit8: UInt8) -> some TextNode {
textStyle(.backgroundColor(bit8: bit8))
}
@inlinable
func backgroundColor(_ red: UInt8, _ green: UInt8, _ blue: UInt8) -> some TextNode {
textStyle(.backgroundColor(rgb: (red, green, blue)))
}
@inlinable
func textStyle<S: TextStyle>(_ styles: S...) -> some TextNode {
styles.reduce(render()) { string, style in
style.render(content: string).render()
style.render(content: .init(string)).render()
}
}
@@ -44,10 +59,20 @@ public extension TextNode {
}
// TODO: Remove string restraint.
public protocol TextStyle: NodeModifier where Content == String {}
public protocol TextStyle: TextModifier where Content == TextStyleConfiguration {}
public struct TextStyleConfiguration {
@usableFromInline
let node: any TextNode
@usableFromInline
init(_ node: any TextNode) {
self.node = node
}
}
public extension TextStyle where Self == StyledText {
@inlinable
static var bold: Self { .init(.bold) }
@@ -63,9 +88,6 @@ public extension TextStyle where Self == StyledText {
@inlinable
static var blink: Self { .init(.blink) }
@inlinable
static var swap: Self { .init(.swap) }
@inlinable
static var strikeThrough: Self { .init(.strikethrough) }
}
@@ -74,37 +96,58 @@ public extension TextStyle where Self == ColorTextStyle {
@inlinable
static func color(_ name: NamedColor) -> Self {
.init(.named(name))
.init(.foreground(.named(name)))
}
@inlinable
static func color(bit8: UInt8) -> Self {
.init(.bit8(bit8))
.init(.foreground(.bit8(bit8)))
}
@inlinable
static func color(rgb: RGB) -> Self {
.init(.bit24(rgb))
.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 {
enum Location {
@usableFromInline
enum Style {
case foreground(ColorType)
case background(ColorType)
case background(BackgroundColorType)
}
@usableFromInline
let color: ColorType
let style: Style
@usableFromInline
init(_ color: ColorType) {
self.color = color
init(_ style: Style) {
self.style = style
}
@inlinable
public func render(content: String) -> some TextNode {
content.applyingColor(color)
public func render(content: TextStyleConfiguration) -> some TextNode {
switch style {
case let .foreground(color):
return content.node.render().applyingColor(color)
case let .background(color):
return content.node.render().applyingBackgroundColor(color)
}
}
}
@@ -118,7 +161,7 @@ public struct StyledText: TextStyle {
}
@inlinable
public func render(content: String) -> some TextNode {
content.applyingStyle(style)
public func render(content: TextStyleConfiguration) -> some TextNode {
content.node.render().applyingStyle(style)
}
}

View File

@@ -9,17 +9,6 @@ func array(from node: any TextNode) -> [any TextNode] {
}
}
@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