Skip to content

Commit 5b4472f

Browse files
committed
Add functionality from TypeScript implementation
1 parent 9d79438 commit 5b4472f

8 files changed

+1224
-276
lines changed

Sources/Ast.swift

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ struct TupleLiteral: Literal {
4141
}
4242

4343
struct ObjectLiteral: Literal {
44-
var value: [(Expression, Expression)]
44+
var value: [String: Expression]
4545
}
4646

4747
struct Set: Statement {
@@ -59,14 +59,14 @@ struct Identifier: Expression {
5959
var value: String
6060
}
6161

62-
protocol Loopvar {}
63-
extension Identifier: Loopvar {}
64-
extension TupleLiteral: Loopvar {}
62+
typealias Loopvar = Expression
6563

6664
struct For: Statement {
6765
var loopvar: Loopvar
6866
var iterable: Expression
6967
var body: [Statement]
68+
var defaultBlock: [Statement]
69+
var ifCondition: Expression?
7070
}
7171

7272
struct MemberExpression: Expression {
@@ -124,3 +124,23 @@ struct KeywordArgumentExpression: Expression {
124124
struct NullLiteral: Literal {
125125
var value: Any? = nil
126126
}
127+
128+
struct SelectExpression: Expression {
129+
var iterable: Expression
130+
var test: Expression
131+
}
132+
133+
struct Macro: Statement {
134+
var name: Identifier
135+
var args: [Expression]
136+
var body: [Statement]
137+
}
138+
139+
struct KeywordArgumentsValue: RuntimeValue {
140+
var value: [String: any RuntimeValue]
141+
var builtins: [String: any RuntimeValue] = [:]
142+
143+
func bool() -> Bool {
144+
!value.isEmpty
145+
}
146+
}

Sources/Environment.swift

Lines changed: 104 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,39 @@ class Environment {
1212

1313
var variables: [String: any RuntimeValue] = [
1414
"namespace": FunctionValue(value: { args, _ in
15-
if args.count == 0 {
15+
if args.isEmpty {
1616
return ObjectValue(value: [:])
1717
}
1818

19-
if args.count != 1 || !(args[0] is ObjectValue) {
19+
guard args.count == 1, let objectArg = args[0] as? ObjectValue else {
2020
throw JinjaError.runtime("`namespace` expects either zero arguments or a single object argument")
2121
}
2222

23-
return args[0]
23+
return objectArg
2424
})
2525
]
2626

2727
var tests: [String: (any RuntimeValue...) throws -> Bool] = [
28-
"boolean": {
29-
args in
30-
args[0] is BooleanValue
28+
"boolean": { args in
29+
return args[0] is BooleanValue
3130
},
3231

33-
"callable": {
34-
args in
35-
args[0] is FunctionValue
32+
"callable": { args in
33+
return args[0] is FunctionValue
3634
},
3735

38-
"odd": {
39-
args in
40-
if let arg = args.first as? NumericValue {
41-
return arg.value as! Int % 2 != 0
36+
"odd": { args in
37+
if let arg = args.first as? NumericValue, let intValue = arg.value as? Int {
38+
return intValue % 2 != 0
4239
} else {
43-
throw JinjaError.runtime("Cannot apply test 'odd' to type: \(type(of:args.first))")
40+
throw JinjaError.runtime("Cannot apply test 'odd' to type: \(type(of: args.first))")
4441
}
4542
},
4643
"even": { args in
47-
if let arg = args.first as? NumericValue {
48-
return arg.value as! Int % 2 == 0
44+
if let arg = args.first as? NumericValue, let intValue = arg.value as? Int {
45+
return intValue % 2 == 0
4946
} else {
50-
throw JinjaError.runtime("Cannot apply test 'even' to type: \(type(of:args.first))")
47+
throw JinjaError.runtime("Cannot apply test 'even' to type: \(type(of: args.first))")
5148
}
5249
},
5350
"false": { args in
@@ -62,24 +59,28 @@ class Environment {
6259
}
6360
return false
6461
},
62+
"string": { args in
63+
return args[0] is StringValue
64+
},
6565
"number": { args in
66-
args[0] is NumericValue
66+
return args[0] is NumericValue
6767
},
6868
"integer": { args in
6969
if let arg = args[0] as? NumericValue {
7070
return arg.value is Int
7171
}
72-
7372
return false
7473
},
74+
"mapping": { args in
75+
return args[0] is ObjectValue
76+
},
7577
"iterable": { args in
76-
args[0] is ArrayValue || args[0] is StringValue
78+
return args[0] is ArrayValue || args[0] is StringValue || args[0] is ObjectValue
7779
},
7880
"lower": { args in
7981
if let arg = args[0] as? StringValue {
8082
return arg.value == arg.value.lowercased()
8183
}
82-
8384
return false
8485
},
8586
"upper": { args in
@@ -89,16 +90,47 @@ class Environment {
8990
return false
9091
},
9192
"none": { args in
92-
args[0] is NullValue
93+
return args[0] is NullValue
9394
},
9495
"defined": { args in
95-
!(args[0] is UndefinedValue)
96+
return !(args[0] is UndefinedValue)
9697
},
9798
"undefined": { args in
98-
args[0] is UndefinedValue
99+
return args[0] is UndefinedValue
99100
},
100-
"equalto": { _ in
101-
throw JinjaError.syntaxNotSupported("equalto")
101+
"equalto": { args in
102+
if args.count == 2 {
103+
if let left = args[0] as? StringValue, let right = args[1] as? StringValue {
104+
return left.value == right.value
105+
} else if let left = args[0] as? NumericValue, let right = args[1] as? NumericValue,
106+
let leftInt = left.value as? Int, let rightInt = right.value as? Int
107+
{
108+
return leftInt == rightInt
109+
} else if let left = args[0] as? BooleanValue, let right = args[1] as? BooleanValue {
110+
return left.value == right.value
111+
} else {
112+
return false
113+
}
114+
} else {
115+
return false
116+
}
117+
},
118+
"eq": { args in
119+
if args.count == 2 {
120+
if let left = args[0] as? StringValue, let right = args[1] as? StringValue {
121+
return left.value == right.value
122+
} else if let left = args[0] as? NumericValue, let right = args[1] as? NumericValue,
123+
let leftInt = left.value as? Int, let rightInt = right.value as? Int
124+
{
125+
return leftInt == rightInt
126+
} else if let left = args[0] as? BooleanValue, let right = args[1] as? BooleanValue {
127+
return left.value == right.value
128+
} else {
129+
return false
130+
}
131+
} else {
132+
return false
133+
}
102134
},
103135
]
104136

@@ -107,61 +139,74 @@ class Environment {
107139
}
108140

109141
func isFunction<T>(_ value: Any, functionType: T.Type) -> Bool {
110-
value is T
142+
return value is T
111143
}
112144

113145
func convertToRuntimeValues(input: Any) throws -> any RuntimeValue {
114146
switch input {
115147
case let value as Bool:
116148
return BooleanValue(value: value)
117-
case let values as [any Numeric]:
118-
var items: [any RuntimeValue] = []
119-
for value in values {
120-
try items.append(self.convertToRuntimeValues(input: value))
121-
}
122-
return ArrayValue(value: items)
123149
case let value as any Numeric:
124150
return NumericValue(value: value)
125151
case let value as String:
126152
return StringValue(value: value)
127153
case let fn as (String) throws -> Void:
128154
return FunctionValue { args, _ in
129-
var arg = ""
130-
switch args[0].value {
131-
case let value as String:
132-
arg = value
133-
case let value as Bool:
134-
arg = String(value)
135-
default:
136-
throw JinjaError.runtime("Unknown arg type:\(type(of: args[0].value))")
155+
guard let stringArg = args[0] as? StringValue else {
156+
throw JinjaError.runtime("Argument must be a StringValue")
137157
}
138-
139-
try fn(arg)
158+
try fn(stringArg.value)
140159
return NullValue()
141160
}
142161
case let fn as (Bool) throws -> Void:
143162
return FunctionValue { args, _ in
144-
try fn(args[0].value as! Bool)
163+
guard let boolArg = args[0] as? BooleanValue else {
164+
throw JinjaError.runtime("Argument must be a BooleanValue")
165+
}
166+
try fn(boolArg.value)
145167
return NullValue()
146168
}
147169
case let fn as (Int, Int?, Int) -> [Int]:
148170
return FunctionValue { args, _ in
149-
let result = fn(args[0].value as! Int, args[1].value as? Int, args[2].value as! Int)
171+
guard let arg0 = args[0] as? NumericValue, let int0 = arg0.value as? Int else {
172+
throw JinjaError.runtime("First argument must be an Int")
173+
}
174+
let int1 = (args[1] as? NumericValue)?.value as? Int
175+
guard let arg2 = args[2] as? NumericValue, let int2 = arg2.value as? Int else {
176+
throw JinjaError.runtime("Third argument must be an Int")
177+
}
178+
let result = fn(int0, int1, int2)
150179
return try self.convertToRuntimeValues(input: result)
151180
}
152-
case let values as [Any]:
153-
var items: [any RuntimeValue] = []
154-
for value in values {
155-
try items.append(self.convertToRuntimeValues(input: value))
181+
case let fn as ([Int]) -> [Int]:
182+
return FunctionValue { args, _ in
183+
let intArgs = args.compactMap { ($0 as? NumericValue)?.value as? Int }
184+
guard intArgs.count == args.count else {
185+
throw JinjaError.runtime("Arguments to range must be Ints")
186+
}
187+
let result = fn(intArgs)
188+
return try self.convertToRuntimeValues(input: result)
189+
}
190+
case let fn as (Int, Int?, Int) -> [Int]:
191+
return FunctionValue { args, _ in
192+
guard let arg0 = args[0] as? NumericValue, let int0 = arg0.value as? Int else {
193+
throw JinjaError.runtime("First argument must be an Int")
194+
}
195+
let int1 = (args.count > 1) ? (args[1] as? NumericValue)?.value as? Int : nil
196+
guard let arg2 = args.last as? NumericValue, let int2 = arg2.value as? Int else {
197+
throw JinjaError.runtime("Last argument must be an Int")
198+
}
199+
let result = fn(int0, int1, int2)
200+
return try self.convertToRuntimeValues(input: result)
156201
}
202+
case let values as [Any]:
203+
let items = try values.map { try self.convertToRuntimeValues(input: $0) }
157204
return ArrayValue(value: items)
158-
case let dictionary as [String: String]:
205+
case let dictionary as [String: Any]:
159206
var object: [String: any RuntimeValue] = [:]
160-
161207
for (key, value) in dictionary {
162-
object[key] = StringValue(value: value)
208+
object[key] = try self.convertToRuntimeValues(input: value)
163209
}
164-
165210
return ObjectValue(value: object)
166211
case is NullValue:
167212
return NullValue()
@@ -176,12 +221,11 @@ class Environment {
176221
}
177222

178223
func declareVariable(name: String, value: any RuntimeValue) throws -> any RuntimeValue {
179-
if self.variables.contains(where: { $0.0 == name }) {
224+
if self.variables.keys.contains(name) {
180225
throw JinjaError.syntax("Variable already declared: \(name)")
181226
}
182227

183228
self.variables[name] = value
184-
185229
return value
186230
}
187231

@@ -191,25 +235,21 @@ class Environment {
191235
return value
192236
}
193237

194-
func resolve(name: String) throws -> Self {
195-
if self.variables.contains(where: { $0.0 == name }) {
238+
func resolve(name: String) throws -> Environment {
239+
if self.variables.keys.contains(name) {
196240
return self
197241
}
198242

199-
if let parent {
200-
return try parent.resolve(name: name) as! Self
243+
if let parent = self.parent {
244+
return try parent.resolve(name: name)
201245
}
202246

203247
throw JinjaError.runtime("Unknown variable: \(name)")
204248
}
205249

206250
func lookupVariable(name: String) -> any RuntimeValue {
207251
do {
208-
if let value = try self.resolve(name: name).variables[name] {
209-
return value
210-
} else {
211-
return UndefinedValue()
212-
}
252+
return try self.resolve(name: name).variables[name] ?? UndefinedValue()
213253
} catch {
214254
return UndefinedValue()
215255
}

Sources/Lexer.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ enum TokenType: String {
5050
case and = "And"
5151
case or = "Or"
5252
case not = "Not"
53+
case macro = "Macro"
54+
case endMacro = "EndMacro"
5355
}
5456

5557
struct Token: Equatable {
@@ -70,6 +72,8 @@ let keywords: [String: TokenType] = [
7072
"and": .and,
7173
"or": .or,
7274
"not": .not,
75+
"macro": .macro,
76+
"endmacro": .endMacro,
7377
// Literals
7478
"true": .booleanLiteral,
7579
"false": .booleanLiteral,

0 commit comments

Comments
 (0)