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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ let project = Folder("MyProject") {
File("main.swift") {
Line {
"print("
Quoted {
Quote {
"Hello, World!"
}
")"
Expand Down
117 changes: 117 additions & 0 deletions Sources/Calligraphy/Calligraphy.docc/Articles/EnvironmentValues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Using the String Environment

@Metadata {
@PageKind(article)
}

Read and write values that flow through a ``StringComponent`` tree.

## Overview

Every ``StringComponent`` is rendered with an instance of ``StringEnvironmentValues`` that flows from ancestor to descendant. This is the same pattern as SwiftUI's `EnvironmentValues`: a parent component can set a value, and any descendant can read it without the value being passed explicitly through initializers.

Calligraphy itself uses the environment to configure built-in components — for example, ``Lines`` reads ``StringEnvironmentValues/lineSpacing`` to decide how many newlines to put between its children, and ``QuotationMark`` reads ``StringEnvironmentValues/quotationMarkStyle`` to choose its character. The same machinery is available to your own components.

## Reading Values

Inside a `StringComponent`, use the ``StringEnvironment`` property wrapper to read a value from the surrounding environment. The wrapper resolves lazily at render time, so its value is always current — and because the property is read inside `body`, you can branch on it normally:

```swift
struct ListItem: StringComponent {

@StringEnvironment(\.lineSpacing)
private var spacing: Int

let text: String

var body: some StringComponent {
if spacing > 1 {
"• \(text)"
} else {
"- \(text)"
}
}

}
```

The property wrapper is populated by reflection on the enclosing `StringComponent`, so it only works as a stored property of a component. When you need to read the environment outside of that context — in a free `@StringBuilder` function, inside a `String.build { ... }` closure, or anywhere else — reach for ``ReadEnvironment`` instead:

```swift
let component = ReadEnvironment { environment in
if environment.lineSpacing > 1 {
"spaced"
} else {
"tight"
}
}
```

## Writing Values

Use the ``StringComponent/environment(_:_:)-(_,Value)`` modifier to set an environment value on a component and its descendants. Ancestor components are unaffected.

```swift
let component = Lines {
"foo"
"bar"
"baz"
}
.environment(\.lineSpacing, 2)
```

When you need to set multiple values at once, or compute the new value from the current one, use ``StringComponent/transformEnvironment(_:)``:

```swift
Lines {
"foo"
"bar"
}
.transformEnvironment { environment in
environment.lineSpacing = 2
environment.tabDefinition = .spaces(4)
}
```

If two modifiers in the same chain write the same value, the one closest to the descendants wins, because it transforms the environment last.

## Defining Custom Environment Values

To expose your own environment value, extend ``StringEnvironmentValues`` and annotate a stored property with the ``StringEntry()`` macro. The macro synthesizes a private ``StringEnvironmentKey`` and the getter/setter accessors that read from and write to the environment storage.

```swift
extension StringEnvironmentValues {

@StringEntry
public var prefix: String = "•"

}
```

The property's initial value becomes the default returned when no ancestor has set the value:

```swift
struct ListItem: StringComponent {

@StringEnvironment(\.prefix)
private var prefix: String

let text: String

var body: some StringComponent {
"\(prefix) \(text)"
}

}
```

Optional types do not need an initial value — when omitted, the default is `nil`:

```swift
extension StringEnvironmentValues {

@StringEntry
public var caption: String?

}
```
15 changes: 7 additions & 8 deletions Sources/Calligraphy/Calligraphy.docc/Calligraphy.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,13 @@ Calligraphy's type-safe API and builder patterns make it ideal for code generati

## Topics

### Getting Started

- <doc:Setup>

### String Composition

- <doc:ComposingStrings>
- ``StringComponent``
- ``StringBuilder``
- ``StringComponents``
Expand All @@ -42,9 +47,7 @@ Calligraphy's type-safe API and builder patterns make it ideal for code generati
- ``Tab``
- ``RawStringComponent``
- ``AnyStringComponent``

### String Composition Environment

- <doc:EnvironmentValues>
- ``ReadEnvironment``
- ``StringEnvironment``
- ``StringEnvironmentKey``
Expand All @@ -53,6 +56,7 @@ Calligraphy's type-safe API and builder patterns make it ideal for code generati

### Directory Composition

- <doc:ComposingDirectories>
- ``DirectoryContent``
- ``DirectoryContentBuilder``
- ``Directory``
Expand All @@ -77,8 +81,3 @@ Calligraphy's type-safe API and builder patterns make it ideal for code generati
- ``EmptyDataComponent``
- ``AnyDataComponent``

### Articles

- <doc:Setup>
- <doc:ComposingStrings>
- <doc:ComposingDirectories>
59 changes: 59 additions & 0 deletions Sources/Calligraphy/StringComposition/ArrayExtensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Calligraphy
// ArrayExtensions.swift
//
// MIT License
//
// Copyright (c) 2026 Varun Santhanam
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
//
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

@available(macOS 14.0, macCatalyst 17.0, iOS 17.0, watchOS 10.0, tvOS 17.0, visionOS 1.0, *)
extension Array {

/// Transform each element of this array into a ``StringComponent`` and combine the results.
///
/// Use this overload to embed an array directly into a `@StringBuilder` block. Because the closure is itself a `@StringBuilder`, the full DSL — including conditionals and nested control flow — is available when shaping each element's contribution.
///
/// ```swift
/// let items = ["apple", "banana", "cherry"]
///
/// let list = String.build {
/// items.map { item in
/// "- \(item)"
/// }
/// }
/// // - apple
/// // - banana
/// // - cherry
/// ```
///
/// - Parameter mapper: A `@StringBuilder` closure that transforms each element into a ``StringComponent``.
/// - Returns: A component that renders `mapper` applied to each element in order.
/// - Note: This overload is `@_disfavoredOverload`, so it participates in overload resolution only when the closure is constrained into returning a ``StringComponent``.
@StringBuilder
@_disfavoredOverload
public func map(
@StringBuilder mapper: (Element) -> some StringComponent
) -> some StringComponent {
for value in self {
mapper(value)
}
}

}
13 changes: 12 additions & 1 deletion Sources/Calligraphy/StringComposition/Entry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,18 @@
/// }
/// ```
///
/// The expanded property reads its default from the initializer expression you provide, and can be set on any component using ``StringComponent/environment(_:_:)-(_,Value)`` or read using ``StringEnvironment``.
/// Non-optional properties must provide an initial value. Optional properties may omit the initial value, in which case the default is `nil`:
///
/// ```swift
/// extension StringEnvironmentValues {
///
/// @StringEntry
/// public var prefix: String? // Default is `nil`, since `String?` is an optional and no default was provided.
///
/// }
/// ```
///
/// The expanded property can be set on any component using ``StringComponent/environment(_:_:)-(_,Value)`` or read using ``StringEnvironment``.
@available(macOS 14.0, macCatalyst 17.0, iOS 17.0, watchOS 10.0, tvOS 17.0, visionOS 1.0, *)
@attached(accessor)
@attached(peer, names: prefixed(__Key_))
Expand Down
2 changes: 1 addition & 1 deletion Sources/Calligraphy/StringComposition/Joined.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ extension StringEnvironmentValues {
///
/// Defaults to `"\n"`. Components such as ``Lines`` read this value to decide how to join their children. Set it on an ancestor component using ``StringComponent/joined(separator:)``.
@StringEntry
public var separator: String = "\n"
public internal(set) var separator: String = "\n"

}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Calligraphy/StringComposition/LineSpacing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ extension StringEnvironmentValues {
///
/// Defaults to `1`. ``Lines`` and components built on top of it read this value to decide how to join their children. Set it on an ancestor component using ``StringComponent/lineSpacing(_:)``.
@StringEntry
public var lineSpacing: Int = 1
public internal(set) var lineSpacing: Int = 1

}

Expand Down
4 changes: 3 additions & 1 deletion Sources/Calligraphy/StringComposition/PrefixLines.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ extension StringComponent {
public func prefixLines(
with prefix: some StringProtocol
) -> some StringComponent {
prefixLines { prefix }
prefixLines {
prefix
}
}

}
Expand Down
15 changes: 12 additions & 3 deletions Sources/Calligraphy/StringComposition/QuotationMark.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,32 @@

/// A string component that renders a quotation mark.
///
/// The character (or characters) rendered are controlled by the surrounding ``QuotationMarkStyle`` environment value. By default, a `QuotationMark` renders as a single double-quote character (`"`). To use a different style, apply the ``StringComponent/quotationMarkStyle(_:)`` modifier to an ancestor component.
/// The character (or characters) rendered are controlled by the surrounding ``QuotationMarkStyle`` environment value. By default, a `QuotationMark` renders as a single double-quote character (`"`). To use a different style, apply the ``StringComponent/quotationMarkStyle(_:)`` modifier to an ancestor component, or pass an explicit style to the initializer to bypass the environment.
@available(macOS 14.0, macCatalyst 17.0, iOS 17.0, watchOS 10.0, tvOS 17.0, visionOS 1.0, *)
public struct QuotationMark: StringComponent {

// MARK: - Initializers

/// Create a quotation mark component.
public init() {}
/// - Parameter style: An optional ``QuotationMarkStyle`` override. When `nil` (the default), the style is read from the surrounding ``StringEnvironmentValues/quotationMarkStyle`` environment value.
public init(_ style: QuotationMarkStyle? = nil) {
self.style = style
}

// MARK: - StringComponent

public var body: some StringComponent {
quotationMarkStyle
if let style {
style
} else {
quotationMarkStyle
}
}

// MARK: - Private

private let style: QuotationMarkStyle?

@StringEnvironment(\.quotationMarkStyle)
private var quotationMarkStyle

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ extension StringEnvironmentValues {
///
/// Defaults to ``QuotationMarkStyle/default``. Set it on an ancestor component using ``StringComponent/quotationMarkStyle(_:)``.
@StringEntry
public var quotationMarkStyle: QuotationMarkStyle = .default
public internal(set) var quotationMarkStyle: QuotationMarkStyle = .default

}

Expand Down
Loading