portico/Sources/Portico/State/InterpolatedText.swift

86 lines
3.4 KiB
Swift

/// A string literal whose interpolated segments are captured unevaluated, so a widget
/// property built from it can be re-evaluated when the state those segments read changes.
///
/// `"plain"` (no interpolation) collapses to ``staticText`` and is applied once with no
/// subscription. `"Count: \(count)"` keeps one deferred closure per `\(...)`; evaluating
/// it inside a ``DependencyTracker`` registers every state box it reads.
public struct InterpolatedText: ExpressibleByStringInterpolation {
fileprivate enum Segment {
case literal(String)
case deferred(() -> String)
}
private let segments: [Segment]
/// The fully resolved text when this value has no interpolations; `nil` when at least
/// one segment must be evaluated on demand. Non-`nil` means "can never change".
@_spi(Portico) public let staticText: String?
/// Creates an ``InterpolatedText`` from a plain string literal with no interpolations.
public init(stringLiteral value: String) {
segments = []
staticText = value
}
/// Creates an ``InterpolatedText`` from a string interpolation, capturing each
/// `\(...)` segment as a deferred closure.
public init(stringInterpolation: StringInterpolation) {
if stringInterpolation.deferredCount == 0 {
var result = ""
for seg in stringInterpolation.segments {
if case .literal(let s) = seg { result += s }
}
staticText = result
segments = []
} else {
segments = stringInterpolation.segments
staticText = nil
}
}
/// Concatenates every segment. Deferred segments run under the active
/// ``DependencyTracker``, so their state reads register as dependencies.
@_spi(Portico) public func evaluate() -> String {
if let s = staticText { return s }
var result = ""
for seg in segments {
switch seg {
case .literal(let s): result += s
case .deferred(let f): result += f()
}
}
return result
}
/// ``evaluate()`` with dependency tracking suppressed.
@_spi(Portico) public var untrackedText: String {
DependencyTracker.untracked { evaluate() }
}
/// Accumulates literal text and deferred interpolation segments for an
/// ``InterpolatedText``. Callers do not construct this directly; the compiler
/// synthesises it from string-interpolation syntax.
public struct StringInterpolation: StringInterpolationProtocol {
fileprivate var segments: [Segment] = []
fileprivate var deferredCount = 0
/// Creates an empty interpolation with capacity hints for the compiler.
public init(literalCapacity: Int, interpolationCount: Int) {
segments.reserveCapacity(interpolationCount * 2 + 1)
}
/// Appends a verbatim run of string content to the interpolation.
public mutating func appendLiteral(_ literal: String) {
segments.append(.literal(literal))
}
/// Captures `value` unevaluated so it can be re-read inside a
/// ``DependencyTracker`` when the property updates.
public mutating func appendInterpolation<Value>(
_ value: @autoclosure @escaping () -> Value
) {
deferredCount += 1
segments.append(.deferred { String(describing: value()) })
}
}
}