Skip to content

Commit 16375f3

Browse files
committed
Add functionality from TypeScript implementation
1 parent 9d79438 commit 16375f3

8 files changed

+1183
-275
lines changed

Sources/Ast.swift

Lines changed: 23 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,13 @@ 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]
7069
}
7170

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

Sources/Environment.swift

Lines changed: 80 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,25 @@ 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
},
7574
"iterable": { args in
76-
args[0] is ArrayValue || args[0] is StringValue
75+
return args[0] is ArrayValue || args[0] is StringValue
7776
},
7877
"lower": { args in
7978
if let arg = args[0] as? StringValue {
8079
return arg.value == arg.value.lowercased()
8180
}
82-
8381
return false
8482
},
8583
"upper": { args in
@@ -89,16 +87,47 @@ class Environment {
8987
return false
9088
},
9189
"none": { args in
92-
args[0] is NullValue
90+
return args[0] is NullValue
9391
},
9492
"defined": { args in
95-
!(args[0] is UndefinedValue)
93+
return !(args[0] is UndefinedValue)
9694
},
9795
"undefined": { args in
98-
args[0] is UndefinedValue
96+
return args[0] is UndefinedValue
9997
},
100-
"equalto": { _ in
101-
throw JinjaError.syntaxNotSupported("equalto")
98+
"equalto": { args in
99+
if args.count == 2 {
100+
if let left = args[0] as? StringValue, let right = args[1] as? StringValue {
101+
return left.value == right.value
102+
} else if let left = args[0] as? NumericValue, let right = args[1] as? NumericValue,
103+
let leftInt = left.value as? Int, let rightInt = right.value as? Int
104+
{
105+
return leftInt == rightInt
106+
} else if let left = args[0] as? BooleanValue, let right = args[1] as? BooleanValue {
107+
return left.value == right.value
108+
} else {
109+
return false
110+
}
111+
} else {
112+
return false
113+
}
114+
},
115+
"eq": { args in
116+
if args.count == 2 {
117+
if let left = args[0] as? StringValue, let right = args[1] as? StringValue {
118+
return left.value == right.value
119+
} else if let left = args[0] as? NumericValue, let right = args[1] as? NumericValue,
120+
let leftInt = left.value as? Int, let rightInt = right.value as? Int
121+
{
122+
return leftInt == rightInt
123+
} else if let left = args[0] as? BooleanValue, let right = args[1] as? BooleanValue {
124+
return left.value == right.value
125+
} else {
126+
return false
127+
}
128+
} else {
129+
return false
130+
}
102131
},
103132
]
104133

@@ -107,61 +136,53 @@ class Environment {
107136
}
108137

109138
func isFunction<T>(_ value: Any, functionType: T.Type) -> Bool {
110-
value is T
139+
return value is T
111140
}
112141

113142
func convertToRuntimeValues(input: Any) throws -> any RuntimeValue {
114143
switch input {
115144
case let value as Bool:
116145
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)
123146
case let value as any Numeric:
124147
return NumericValue(value: value)
125148
case let value as String:
126149
return StringValue(value: value)
127150
case let fn as (String) throws -> Void:
128151
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))")
152+
guard let stringArg = args[0] as? StringValue else {
153+
throw JinjaError.runtime("Argument must be a StringValue")
137154
}
138-
139-
try fn(arg)
155+
try fn(stringArg.value)
140156
return NullValue()
141157
}
142158
case let fn as (Bool) throws -> Void:
143159
return FunctionValue { args, _ in
144-
try fn(args[0].value as! Bool)
160+
guard let boolArg = args[0] as? BooleanValue else {
161+
throw JinjaError.runtime("Argument must be a BooleanValue")
162+
}
163+
try fn(boolArg.value)
145164
return NullValue()
146165
}
147166
case let fn as (Int, Int?, Int) -> [Int]:
148167
return FunctionValue { args, _ in
149-
let result = fn(args[0].value as! Int, args[1].value as? Int, args[2].value as! Int)
168+
guard let arg0 = args[0] as? NumericValue, let int0 = arg0.value as? Int else {
169+
throw JinjaError.runtime("First argument must be an Int")
170+
}
171+
let int1 = (args[1] as? NumericValue)?.value as? Int
172+
guard let arg2 = args[2] as? NumericValue, let int2 = arg2.value as? Int else {
173+
throw JinjaError.runtime("Third argument must be an Int")
174+
}
175+
let result = fn(int0, int1, int2)
150176
return try self.convertToRuntimeValues(input: result)
151177
}
152178
case let values as [Any]:
153-
var items: [any RuntimeValue] = []
154-
for value in values {
155-
try items.append(self.convertToRuntimeValues(input: value))
156-
}
179+
let items = try values.map { try self.convertToRuntimeValues(input: $0) }
157180
return ArrayValue(value: items)
158-
case let dictionary as [String: String]:
181+
case let dictionary as [String: Any]:
159182
var object: [String: any RuntimeValue] = [:]
160-
161183
for (key, value) in dictionary {
162-
object[key] = StringValue(value: value)
184+
object[key] = try self.convertToRuntimeValues(input: value)
163185
}
164-
165186
return ObjectValue(value: object)
166187
case is NullValue:
167188
return NullValue()
@@ -176,12 +197,11 @@ class Environment {
176197
}
177198

178199
func declareVariable(name: String, value: any RuntimeValue) throws -> any RuntimeValue {
179-
if self.variables.contains(where: { $0.0 == name }) {
200+
if self.variables.keys.contains(name) {
180201
throw JinjaError.syntax("Variable already declared: \(name)")
181202
}
182203

183204
self.variables[name] = value
184-
185205
return value
186206
}
187207

@@ -191,25 +211,21 @@ class Environment {
191211
return value
192212
}
193213

194-
func resolve(name: String) throws -> Self {
195-
if self.variables.contains(where: { $0.0 == name }) {
214+
func resolve(name: String) throws -> Environment {
215+
if self.variables.keys.contains(name) {
196216
return self
197217
}
198218

199-
if let parent {
200-
return try parent.resolve(name: name) as! Self
219+
if let parent = self.parent {
220+
return try parent.resolve(name: name)
201221
}
202222

203223
throw JinjaError.runtime("Unknown variable: \(name)")
204224
}
205225

206226
func lookupVariable(name: String) -> any RuntimeValue {
207227
do {
208-
if let value = try self.resolve(name: name).variables[name] {
209-
return value
210-
} else {
211-
return UndefinedValue()
212-
}
228+
return try self.resolve(name: name).variables[name] ?? UndefinedValue()
213229
} catch {
214230
return UndefinedValue()
215231
}

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)