86 lines
3.4 KiB
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()) })
|
|
}
|
|
}
|
|
}
|