Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/Benchmarks/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import SwiftUI
import TextBuilder

benchmark("@TextBuilder") {
@SpacedTextBuilder
@TextBuilderWithSpaces
func complexTextBuilderText() -> Text {
"Lorem".text.underline().foregroundColor(.blue)
if `false` {
Expand Down
62 changes: 41 additions & 21 deletions Sources/TextBuilder/TextBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import SwiftUI
/// text separated using eggplant emoji.
///
/// struct EggplantSeparator: TextBuilderSeparator {
/// static var separator: String { " 🍆 " }
/// static var separator: String? { " 🍆 " }
/// }
///
/// @TextBuilder<EggplantSeparator>
Expand All @@ -21,52 +21,68 @@ import SwiftUI
///
#if compiler(>=5.7)
@resultBuilder
public struct TextBuilder<Separator: TextBuilderSeparator> {
public static func buildPartialBlock(first: Text) -> Text {
public struct TextBuilderWith<Separator: TextBuilderSeparator> {
@inlinable
public static func buildPartialBlock(first: Text?) -> Text? {
first
}

public static func buildPartialBlock(accumulated: Text, next: Text) -> Text {
if next.isNone {
@inlinable
public static func buildPartialBlock(accumulated: Text?, next: Text?) -> Text? {
guard let next else {
return accumulated
} else if Separator.separator.isEmpty {
}
guard let accumulated else {
return next
}
guard let separator = Separator.separator else {
return accumulated + next
} else {
return accumulated + Text(Separator.separator) + next
}
return accumulated + Text(separator) + next
}

public static func buildArray(_ components: [Text]) -> Text {
components.joined(separator: Text(Separator.separator))
public static func buildArray(_ components: [Text?]) -> Text? {
components.lazy.compactMap { $0 }.joined(separator: Separator.separator.map(Text.init))
}

public static func buildEither(first component: Text) -> Text {
@inlinable
public static func buildEither(first component: Text?) -> Text? {
component
}

public static func buildEither(second component: Text) -> Text {
@inlinable
public static func buildEither(second component: Text?) -> Text? {
component
}

public static func buildExpression(_ string: some StringProtocol) -> Text {
@inlinable
public static func buildExpression(_ string: some StringProtocol) -> Text? {
Text(string)
}

public static func buildExpression(_ component: Text) -> Text {
@inlinable
public static func buildExpression(_ component: Text?) -> Text? {
component
}

public static func buildLimitedAvailability(_ component: Text) -> Text {
@inlinable
public static func buildLimitedAvailability(_ component: Text?) -> Text? {
component
}

public static func buildOptional(_ component: Text?) -> Text {
component ?? Text.none
@inlinable
public static func buildOptional(_ component: Text??) -> Text? {
component ?? nil
}

@inlinable
public static func buildFinalResult(_ component: Text?) -> Text {
component ?? Text(verbatim: "")
}
}
#else
@resultBuilder
public struct TextBuilder<Separator: TextBuilderSeparator> {
public struct TextBuilderWith<Separator: TextBuilderSeparator> {
public static func buildArray(_ texts: [[Text]]) -> [Text] {
texts.flatMap { $0 }
}
Expand All @@ -87,8 +103,12 @@ public struct TextBuilder<Separator: TextBuilderSeparator> {
[Text(string)]
}

public static func buildExpression(_ text: Text) -> [Text] {
[text]
public static func buildExpression(_ text: Text?) -> [Text] {
if let text = text {
return [text]
} else {
return []
}
}

public static func buildLimitedAvailability(_ texts: [Text]) -> [Text] {
Expand All @@ -100,7 +120,7 @@ public struct TextBuilder<Separator: TextBuilderSeparator> {
}

public static func buildFinalResult(_ texts: [Text]) -> Text {
texts.joined(separator: Text(Separator.separator))
texts.joined(separator: Separator.separator.map(Text.init)) ?? Text(verbatim: "")
}
}
#endif
16 changes: 8 additions & 8 deletions Sources/TextBuilder/TextBuilderSeparator.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
public protocol TextBuilderSeparator {
associatedtype Separator: StringProtocol
static var separator: Separator { get }
static var separator: Separator? { get }
}

public struct EmptySeparator: TextBuilderSeparator {
public static var separator: String { "" }
public struct NoSeparator: TextBuilderSeparator {
public static var separator: String? { nil }
}

public struct WhitespaceSeparator: TextBuilderSeparator {
public static var separator: String { " " }
public static var separator: String? { " " }
}

public struct NewlineSeparator: TextBuilderSeparator {
public static var separator: String { "\n" }
public static var separator: String? { "\n" }
}

public typealias BasicTextBuilder = TextBuilder<EmptySeparator>
public typealias SpacedTextBuilder = TextBuilder<WhitespaceSeparator>
public typealias MultilineTextBuilder = TextBuilder<NewlineSeparator>
public typealias TextBuilder = TextBuilderWith<NoSeparator>
public typealias TextBuilderWithSpaces = TextBuilderWith<WhitespaceSeparator>
public typealias TextBuilderWithNewlines = TextBuilderWith<NewlineSeparator>
43 changes: 17 additions & 26 deletions Sources/TextBuilder/TextExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,10 @@ extension StringProtocol {
}
}

extension Text {
static var none: Text {
Text(_EmptyTextMarker())
}

var isNone: Bool {
self == .none
}

var isEmpty: Bool {
self == Text(verbatim: "") || self == Text("")
}
}

extension Sequence where Element == Text {
/// Returns a new `Text` by concatenating the elements of the sequence,
/// Returns a new `Text` by concatenating the elements of the sequence.
///
/// If the sequence is empty, it returns nil.
///
/// The following example shows how an array of `Text` views can be joined to a
/// single `Text` view with comma-separated string:
Expand All @@ -35,46 +23,49 @@ extension Sequence where Element == Text {
/// - Parameter separator: A `Text` view to insert between each of the elements
/// in this sequence. By default there is no separator.
/// - Returns: A single, concatenated `Text` view.
public func joined(separator: Text = Text(verbatim: "")) -> Text {
reduce(Text.none) { accumulated, next in
if accumulated.isNone {
public func joined(separator: Text? = nil) -> Text? {
reduce(nil) { accumulated, next in
guard let accumulated = accumulated else {
return next
} else if separator.isEmpty {
}
guard let separator = separator else {
return accumulated + next
} else {
return accumulated + separator + next
}
return accumulated + separator + next
}
}
}

public typealias TextArrayBuilder = ArrayBuilder<Text>

extension TextArrayBuilder {
@inlinable
public static func buildExpression<S: StringProtocol>(_ expression: S) -> [Text] {
[Text(expression)]
}
}

extension Text {
/// Creates a combined text view based on the given `content` by inserting
/// Creates a combined text view based on the given `content` by inserting
/// `separator` text views between each received text component.
///
/// - Parameters:
/// - separator: The text to use as a separator between received text components.
/// By default there is no separator.
/// - content: A text array builder that creates text components.
public init(separator: Text = Text(verbatim: ""), @TextArrayBuilder content: () -> [Text]) {
self = content().joined(separator: separator)
public init?(separator: Text? = nil, @TextArrayBuilder content: () -> [Text]) {
let text = content().joined(separator: separator)
if let text = text { self = text } else { return nil }
}

/// Creates a combined text view based on the given `content` by inserting
/// Creates a combined text view based on the given `content` by inserting
/// `separator` string between each received text component.
///
/// - Parameters:
/// - separator: The string to use as a separator between received text components.
/// - content: A text array builder that creates text components.
public init<Separator: StringProtocol>(separator: Separator, @TextArrayBuilder content: () -> [Text]) {
@inlinable
public init?<Separator: StringProtocol>(separator: Separator, @TextArrayBuilder content: () -> [Text]) {
self.init(separator: Text(separator), content: content)
}
}
154 changes: 0 additions & 154 deletions Sources/TextBuilder/_EmptyTextMarker.swift

This file was deleted.

Loading