feat: Adds stacks, working on styles
This commit is contained in:
30
Sources/CliDoc/Modifiers/NoteStyleModifier.swift
Normal file
30
Sources/CliDoc/Modifiers/NoteStyleModifier.swift
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Rainbow
|
||||||
|
|
||||||
|
public struct NoteStyleConfiguration {
|
||||||
|
let label: any TextNode
|
||||||
|
let content: any TextNode
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Note {
|
||||||
|
func noteStyle<S: NoteStyleModifier>(_ 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).style(.bold)
|
||||||
|
content.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ public protocol NodeModifier {
|
|||||||
typealias Body = _Body
|
typealias Body = _Body
|
||||||
// swiftlint:enable type_name
|
// swiftlint:enable type_name
|
||||||
|
|
||||||
associatedtype Content: TextNode
|
associatedtype Content
|
||||||
|
|
||||||
@TextBuilder
|
@TextBuilder
|
||||||
func render(content: Content) -> Body
|
func render(content: Content) -> Body
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public struct Examples<Header: TextNode, Label: TextNode>: TextNode {
|
|||||||
@usableFromInline
|
@usableFromInline
|
||||||
let label: Label
|
let label: Label
|
||||||
|
|
||||||
|
@inlinable
|
||||||
public init(
|
public init(
|
||||||
examples: [Example],
|
examples: [Example],
|
||||||
@TextBuilder header: () -> Header,
|
@TextBuilder header: () -> Header,
|
||||||
@@ -22,19 +23,42 @@ public struct Examples<Header: TextNode, Label: TextNode>: TextNode {
|
|||||||
self.label = label()
|
self.label = label()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
public var body: some TextNode {
|
public var body: some TextNode {
|
||||||
Group(separator: "") {
|
VStack(spacing: 2) {
|
||||||
Group(separator: " ", content: [header.color(.yellow).style(.bold), label, "\n"])
|
HStack {
|
||||||
"\n"
|
header
|
||||||
Group(
|
label
|
||||||
separator: "\n\n",
|
}
|
||||||
content: self.examples.map { example in
|
VStack(spacing: 2) {
|
||||||
Group(separator: "\n") {
|
self.examples.map { example in
|
||||||
|
VStack {
|
||||||
CliDoc.Label(example.label.green.bold)
|
CliDoc.Label(example.label.green.bold)
|
||||||
ShellCommand { example.example.italic }
|
ShellCommand { example.example.italic }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Examples where Header == String, Label == String {
|
||||||
|
@inlinable
|
||||||
|
init(
|
||||||
|
header: String = "Examples:".yellow.bold,
|
||||||
|
label: String = "Some common usage examples.",
|
||||||
|
examples: [Example]
|
||||||
|
) {
|
||||||
|
self.init(examples: examples) { header } label: { label }
|
||||||
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
init(
|
||||||
|
header: String = "Examples:".yellow.bold,
|
||||||
|
label: String = "Some common usage examples.",
|
||||||
|
examples: Example...
|
||||||
|
) {
|
||||||
|
self.init(header: header, label: label, examples: examples)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,41 +1,16 @@
|
|||||||
public struct Group: TextNode {
|
public struct Group<Content: TextNode>: TextNode {
|
||||||
@usableFromInline
|
@usableFromInline
|
||||||
var content: [any TextNode]
|
var content: Content
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
var separator: any TextNode
|
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public init(
|
public init(
|
||||||
separator: any TextNode = "\n",
|
@TextBuilder content: () -> Content
|
||||||
content: [any TextNode]
|
|
||||||
) {
|
) {
|
||||||
self.content = content
|
self.content = content()
|
||||||
self.separator = separator
|
|
||||||
}
|
|
||||||
|
|
||||||
@inlinable
|
|
||||||
public init(
|
|
||||||
separator: any TextNode = "\n",
|
|
||||||
@TextBuilder content: () -> any TextNode
|
|
||||||
) {
|
|
||||||
// Check if the content is a NodeContainer, typically is when
|
|
||||||
// using the TextBuilder with more than one text node.
|
|
||||||
//
|
|
||||||
// We need to take over the contents, so we can control the separator.
|
|
||||||
let content = content()
|
|
||||||
if let many = content as? NodeContainer {
|
|
||||||
self.content = many.nodes
|
|
||||||
} else {
|
|
||||||
// We didn't get a NodeContainer, so fallback to just storing
|
|
||||||
// the content.
|
|
||||||
self.content = [content]
|
|
||||||
}
|
|
||||||
self.separator = separator
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public var body: some TextNode {
|
public var body: some TextNode {
|
||||||
content.map { $0.render() }.joined(separator: separator.render())
|
content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
Sources/CliDoc/Nodes/HStack.swift
Normal file
22
Sources/CliDoc/Nodes/HStack.swift
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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.map { $0.render() }.joined(separator: separator.render())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,23 +7,21 @@ public struct Note<Label: TextNode, Content: TextNode>: TextNode {
|
|||||||
@usableFromInline
|
@usableFromInline
|
||||||
let content: Content
|
let content: Content
|
||||||
|
|
||||||
@usableFromInline
|
|
||||||
var separator: any TextNode = " "
|
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public init(
|
public init(
|
||||||
separator: any TextNode = " ",
|
|
||||||
@TextBuilder _ label: () -> Label,
|
@TextBuilder _ label: () -> Label,
|
||||||
@TextBuilder content: () -> Content
|
@TextBuilder content: () -> Content
|
||||||
) {
|
) {
|
||||||
self.separator = separator
|
|
||||||
self.label = label()
|
self.label = label()
|
||||||
self.content = content()
|
self.content = content()
|
||||||
}
|
}
|
||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
public var body: some TextNode {
|
public var body: some TextNode {
|
||||||
Group(separator: separator, content: [label, content])
|
HStack {
|
||||||
|
label
|
||||||
|
content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,28 +29,49 @@ public extension Note where Label == String {
|
|||||||
|
|
||||||
@inlinable
|
@inlinable
|
||||||
init(
|
init(
|
||||||
separator: any TextNode = " ",
|
_ label: String = "NOTE:",
|
||||||
_ label: String = "NOTE:".yellow.bold,
|
|
||||||
@TextBuilder content: () -> Content
|
@TextBuilder content: () -> Content
|
||||||
) {
|
) {
|
||||||
self.separator = separator
|
|
||||||
self.label = label
|
self.label = label
|
||||||
self.content = content()
|
self.content = content()
|
||||||
}
|
}
|
||||||
|
|
||||||
static func important(
|
static func important(
|
||||||
separator: any TextNode = " ",
|
_ label: String = "IMPORTANT NOTE:",
|
||||||
_ label: String = "IMPORTANT NOTE:".red.underline,
|
|
||||||
@TextBuilder content: () -> Content
|
@TextBuilder content: () -> Content
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.init(separator: separator, label, content: content)
|
self.init(label, content: content)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func seeAlso(
|
static func seeAlso(
|
||||||
separator: any TextNode = " ",
|
_ label: String = "SEE ALSO:",
|
||||||
_ label: String = "SEE ALSO:".yellow.bold,
|
|
||||||
@TextBuilder content: () -> Content
|
@TextBuilder content: () -> Content
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.init(separator: separator, label, content: content)
|
self.init(label, content: content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Note where Label == String, Content == String {
|
||||||
|
|
||||||
|
@inlinable
|
||||||
|
init(
|
||||||
|
_ label: String = "NOTE:",
|
||||||
|
content: String
|
||||||
|
) {
|
||||||
|
self.init(label) { content }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func important(
|
||||||
|
_ label: String = "IMPORTANT NOTE:",
|
||||||
|
content: String
|
||||||
|
) -> Self {
|
||||||
|
self.init(label, content: content)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func seeAlso(
|
||||||
|
_ label: String = "SEE ALSO:",
|
||||||
|
content: String
|
||||||
|
) -> Self {
|
||||||
|
self.init(label, content: content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ public struct ShellCommand<Content: TextNode>: TextNode {
|
|||||||
self.content = content()
|
self.content = content()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@inlinable
|
||||||
public var body: some TextNode {
|
public var body: some TextNode {
|
||||||
Group(separator: " ", content: [symbol, content])
|
HStack {
|
||||||
|
symbol
|
||||||
|
content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
Sources/CliDoc/Nodes/VStack.swift
Normal file
22
Sources/CliDoc/Nodes/VStack.swift
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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.map { $0.render() }.joined(separator: separator.render())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,3 +59,15 @@ extension Optional: NodeRepresentable where Wrapped: NodeRepresentable {
|
|||||||
return node.render()
|
return node.render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Array: TextNode where Element: TextNode {
|
||||||
|
public var body: some TextNode {
|
||||||
|
NodeContainer(nodes: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Array: NodeRepresentable where Element: NodeRepresentable {
|
||||||
|
public func render() -> String {
|
||||||
|
map { $0.render() }.joined()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
21
Sources/CliDoc/Utils.swift
Normal file
21
Sources/CliDoc/Utils.swift
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
@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
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
@testable import CliDoc2
|
|
||||||
@preconcurrency import Rainbow
|
|
||||||
import Testing
|
|
||||||
|
|
||||||
let setupRainbow: Bool = {
|
|
||||||
Rainbow.enabled = true
|
|
||||||
Rainbow.outputTarget = .console
|
|
||||||
return true
|
|
||||||
}()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
func testGroup() {
|
|
||||||
#expect(setupRainbow)
|
|
||||||
let group = Group {
|
|
||||||
Label { "Foo:" }
|
|
||||||
"Bar"
|
|
||||||
"Baz"
|
|
||||||
Note { "Bang:" } content: { "boom" }
|
|
||||||
if setupRainbow {
|
|
||||||
Label("Hello, rainbow").color(.blue)
|
|
||||||
} else {
|
|
||||||
Label("No color for you!").color(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.color(.green)
|
|
||||||
.style(.italic)
|
|
||||||
|
|
||||||
print(type(of: group))
|
|
||||||
print(group.render())
|
|
||||||
|
|
||||||
// let note = Note { "Bang:" } content: { "boom" }
|
|
||||||
// print(note.render())
|
|
||||||
// print(type(of: note.label))
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
@preconcurrency import Rainbow
|
@preconcurrency import Rainbow
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
|
// Ensure that rainbow is setup, for test comparisons to work properly.
|
||||||
let setupRainbow: Bool = {
|
let setupRainbow: Bool = {
|
||||||
Rainbow.enabled = true
|
Rainbow.enabled = true
|
||||||
Rainbow.outputTarget = .console
|
Rainbow.outputTarget = .console
|
||||||
@@ -12,31 +13,60 @@ let setupRainbow: Bool = {
|
|||||||
func testGroup() {
|
func testGroup() {
|
||||||
#expect(setupRainbow)
|
#expect(setupRainbow)
|
||||||
let group = Group {
|
let group = Group {
|
||||||
Label { "Foo:" }
|
"foo"
|
||||||
"Bar"
|
"bar"
|
||||||
"Baz"
|
|
||||||
Note { "Bang:" } content: { "boom" }
|
|
||||||
if setupRainbow {
|
|
||||||
Label("Hello, rainbow").color(.blue)
|
|
||||||
} else {
|
|
||||||
Label("No color for you!").color(.red)
|
|
||||||
}
|
}
|
||||||
|
#expect(group.render() == "foobar")
|
||||||
}
|
}
|
||||||
.color(.green)
|
|
||||||
.style(.italic)
|
|
||||||
|
|
||||||
print(type(of: group))
|
@Test
|
||||||
print(group.render())
|
func testHStack() {
|
||||||
|
#expect(setupRainbow)
|
||||||
|
let stack = HStack {
|
||||||
|
"foo"
|
||||||
|
"bar"
|
||||||
|
}
|
||||||
|
#expect(stack.render() == "foo bar")
|
||||||
|
}
|
||||||
|
|
||||||
// let note = Note { "Bang:" } content: { "boom" }
|
@Test
|
||||||
// print(note.render())
|
func testVStack() {
|
||||||
// print(type(of: note.label))
|
#expect(setupRainbow)
|
||||||
|
let stack = VStack {
|
||||||
|
"foo"
|
||||||
|
"bar"
|
||||||
|
}
|
||||||
|
#expect(stack.render() == """
|
||||||
|
foo
|
||||||
|
bar
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func testNote() {
|
||||||
|
#expect(setupRainbow)
|
||||||
|
let note = Note(content: "Some note.").noteStyle(.default)
|
||||||
|
let expected = """
|
||||||
|
\("NOTE:".yellow.bold) Some note.
|
||||||
|
"""
|
||||||
|
#expect(note.render() == expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func testExamples() {
|
func testExamples() {
|
||||||
#expect(setupRainbow)
|
#expect(setupRainbow)
|
||||||
let examples = Examples(examples: [("First", "ls -lah"), ("Second", "find . -name foo")], header: { "Examples:" }, label: { "Common examples." })
|
let examples = Examples(
|
||||||
|
examples: [("First", "ls -lah"), ("Second", "find . -name foo")]
|
||||||
|
)
|
||||||
|
|
||||||
print(examples.render())
|
let expected = """
|
||||||
|
\("Examples:".yellow.bold) Some common usage examples.
|
||||||
|
|
||||||
|
\("First".green.bold)
|
||||||
|
$ \("ls -lah".italic)
|
||||||
|
|
||||||
|
\("Second".green.bold)
|
||||||
|
$ \("find . -name foo".italic)
|
||||||
|
"""
|
||||||
|
#expect(examples.render() == expected)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user