-
Notifications
You must be signed in to change notification settings - Fork 1
/
Calculation.swift
168 lines (142 loc) · 6.11 KB
/
Calculation.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//
// Calculation.swift
// Sass
//
// Licensed under MIT (https://github.com/johnfairh/swift-sass/blob/main/LICENSE
//
/// A Sass calculation value.
///
/// These correspond to Sass `calc()`, `min()`, `max()`, or `clamp()` expressions.
/// See [the Sass docs](https://sass-lang.com/documentation/values/calculations).
///
/// The Sass compiler simplifies these before sending them to custom functions: this means that if you
/// receive a `SassCalculation` argument then it cannot be further simplified at compile time,
/// for example `calc(20px + 30%)`.
///
/// The API here allows you to construct `SassCalculation`s representing `calc()`-type expressions
/// including invalid ones such as `calc(20px, 30px)` as though you were writing a stylesheet. The
/// validity is checked -- and the overall expression simplified -- by the compiler when it receives the value.
public final class SassCalculation: SassValue {
// MARK: Types
/// The kind of the `SassCalculation` expression
public enum Kind: String {
/// Sass [`calc()`](https://sass-lang.com/documentation/values/calculations)
case calc
/// Sass [`min()`](https://sass-lang.com/documentation/modules/math#min)
case min
/// Sass [`max()`](https://sass-lang.com/documentation/modules/math#max)
case max
/// Sass [`clamp()`](https://sass-lang.com/documentation/modules/math#clamp)
case clamp
}
/// Arithmetic operators valid within `Value.operation(_:_:_:)`.
public enum Operator: Character {
/// The regular arithmetic operator with normal precedence.
case plus = "+", minus = "-", times = "*", dividedBy = "/"
var isHighPrecedence: Bool {
self == .times || self == .dividedBy
}
var isLowPrecedence: Bool {
!isHighPrecedence
}
}
/// A subexpression of a `SassCalculation`.
public indirect enum Value: Hashable, Equatable, CustomStringConvertible {
/// A number with optional associated units. See `number(_:unit:)`.
case number(SassNumber)
/// A string - expected to be a Sass variable for later evaluation, for example the `$width` part of `calc($width / 2)`.
case string(String)
/// A string - expected to be a Sass variable, but this form means the stylesheet has `#{$width}` instead of `$width`.
/// Use `string(_:)` instead when building your own `Value`s.
case interpolation(String)
/// A binary arithmetic expression.
case operation(Value, Operator, Value)
/// A nested calculation.
case calculation(SassCalculation)
// MARK: Helpers
/// A helper to construct `number(_:)`s.
public static func number(_ double: Double, unit: String? = nil) -> Value {
.number(SassNumber(double, unit: unit))
}
// MARK: Misc
var isLowPrecedenceOperation: Bool {
if case let .operation(_, op, _) = self {
return op.isLowPrecedence
}
return false
}
/// A human-readable description of the value.
public var description: String {
switch self {
case let .number(n): return n.sassDescription
case let .string(s): return s.description
case let .interpolation(s): return "#{\(s)}"
case let .calculation(c): return c.sassDescription
case let .operation(left, op, right):
func valueDescription(_ value: Value) -> String {
let parens = op.isHighPrecedence && value.isLowPrecedenceOperation
let valueDesc = value.description
return parens ? "(\(valueDesc))" : valueDesc
}
return "\(valueDescription(left)) \(op.rawValue) \(valueDescription(right))"
}
}
}
// MARK: Initializers
/// Create a Sass `calc()` expression.
public convenience init(calc value: Value) {
self.init(kind: .calc, arguments: [value])
}
/// Create an arbitrary `SassCalculation`.
public init(kind: Kind, arguments: [Value]) {
self.kind = kind
self.arguments = arguments
}
// MARK: Properties
/// The `SassCalculation`'s `Kind`.
public let kind: Kind
/// The `SassCalculation`'s arguments. The Sass specification says how many
/// are actually valid for each `Kind` but this API does not check this.
public let arguments: [Value]
// MARK: Methods
// Uh I don't think there are any useful methods to offer here.
// Any kind of content-munging or inspection is done via normal
// collection stuff on `arguments`.
//
// Could take a stab at simplification but not sure it's useful
// on the 'host' side.
// MARK: Misc
/// Two `SassCalculation`s are equal if they have the same kind and arguments.
///
/// There's no attempt at simplification here -- so `calc(10px)` and `calc(5px + 5px)`
/// compare as different.
public static func == (lhs: SassCalculation, rhs: SassCalculation) -> Bool {
lhs.kind == rhs.kind && lhs.arguments == rhs.arguments
}
/// Take part in the `SassValueVisitor` protocol.
public override func accept<V, R>(visitor: V) throws -> R where V : SassValueVisitor, R == V.ReturnType {
try visitor.visit(calculation: self)
}
var sassDescription: String {
let args = arguments.map(\.description).joined(separator: ", ")
return "\(kind.rawValue)(\(args))"
}
public override var description: String {
"Calculation(\(sassDescription))"
}
/// Hash the calculation.
public override func hash(into hasher: inout Hasher) {
hasher.combine(kind)
hasher.combine(arguments)
}
}
extension SassValue {
/// Reinterpret the value as a calculation.
/// - throws: `SassFunctionError.wrongType(...)` if it isn't a calculation.
public func asCalculation() throws -> SassCalculation {
guard let selfCalculation = self as? SassCalculation else {
throw SassFunctionError.wrongType(expected: "SassCalculation", actual: self)
}
return selfCalculation
}
}