Skip to content

Commit

Permalink
Add functionality from TypeScript implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
DePasqualeOrg committed Dec 11, 2024
1 parent 6dbe4c4 commit b11a959
Show file tree
Hide file tree
Showing 5 changed files with 673 additions and 368 deletions.
21 changes: 21 additions & 0 deletions Sources/Ast.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ struct For: Statement {
var loopvar: Loopvar
var iterable: Expression
var body: [Statement]
var defaultBlock: [Statement]
}

struct MemberExpression: Expression {
Expand Down Expand Up @@ -124,3 +125,23 @@ struct KeywordArgumentExpression: Expression {
struct NullLiteral: Literal {
var value: Any? = nil
}

struct SelectExpression: Expression {
var iterable: Expression
var test: Expression
}

struct Macro: Statement {
var name: Identifier
var args: [Expression]
var body: [Statement]
}

struct KeywordArgumentsValue: RuntimeValue {
var value: [String: any RuntimeValue]
var builtins: [String: any RuntimeValue] = [:]

func bool() -> Bool {
!value.isEmpty
}
}
184 changes: 86 additions & 98 deletions Sources/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,42 +15,33 @@ class Environment {
if args.count == 0 {
return ObjectValue(value: [:])
}

if args.count != 1 || !(args[0] is ObjectValue) {
throw JinjaError.runtime("`namespace` expects either zero arguments or a single object argument")
}

return args[0]
}),
"range": FunctionValue(value: { args, _ in
let start = args[0].value as! Int
let stop = args.count > 1 ? args[1].value as? Int : nil
let step = args.count > 2 ? args[2].value as! Int : 1
return ArrayValue(value: range(start: start, stop: stop, step: step).map { NumericValue(value: $0) })
})
]

var tests: [String: (any RuntimeValue...) throws -> Bool] = [
"boolean": {
args in
args[0] is BooleanValue
},

"callable": {
args in
args[0] is FunctionValue
},

"odd": {
args in
"boolean": { args in args[0] is BooleanValue },
"callable": { args in args[0] is FunctionValue },
"odd": { args in
if let arg = args.first as? NumericValue {
return arg.value as! Int % 2 != 0
}
else {
throw JinjaError.runtime("Cannot apply test 'odd' to type: \(type(of:args.first))")
}
throw JinjaError.runtime("Cannot apply test 'odd' to type: \(type(of:args.first))")
},
"even": { args in
if let arg = args.first as? NumericValue {
return arg.value as! Int % 2 == 0
}
else {
throw JinjaError.runtime("Cannot apply test 'even' to type: \(type(of:args.first))")
}
throw JinjaError.runtime("Cannot apply test 'even' to type: \(type(of:args.first))")
},
"false": { args in
if let arg = args[0] as? BooleanValue {
Expand All @@ -64,24 +55,26 @@ class Environment {
}
return false
},
"number": { args in
args[0] is NumericValue
},
"number": { args in args[0] is NumericValue },
"integer": { args in
if let arg = args[0] as? NumericValue {
return arg.value is Int
}

return false
},
"iterable": { args in
args[0] is ArrayValue || args[0] is StringValue
"float": { args in
if let num = args[0] as? NumericValue {
return num.value is Double
}
return false
},
"iterable": { args in args[0] is ArrayValue || args[0] is StringValue },
"sequence": { args in args[0] is ArrayValue || args[0] is StringValue },
"mapping": { args in args[0] is ObjectValue },
"lower": { args in
if let arg = args[0] as? StringValue {
return arg.value == arg.value.lowercased()
}

return false
},
"upper": { args in
Expand All @@ -90,17 +83,15 @@ class Environment {
}
return false
},
"none": { args in
args[0] is NullValue
},
"defined": { args in
!(args[0] is UndefinedValue)
},
"undefined": { args in
args[0] is UndefinedValue
"none": { args in args[0] is NullValue },
"defined": { args in !(args[0] is UndefinedValue) },
"undefined": { args in args[0] is UndefinedValue },
"string": { args in args[0] is StringValue },
"equalto": { args in
args[0].value as? AnyHashable == args[1].value as? AnyHashable
},
"equalto": { _ in
throw JinjaError.syntaxNotSupported("equalto")
"eq": { args in
args[0].value as? AnyHashable == args[1].value as? AnyHashable
},
]

Expand All @@ -114,61 +105,64 @@ class Environment {

func convertToRuntimeValues(input: Any) throws -> any RuntimeValue {
switch input {
case let value as Bool:
return BooleanValue(value: value)
case let values as [any Numeric]:
var items: [any RuntimeValue] = []
for value in values {
try items.append(self.convertToRuntimeValues(input: value))
}
return ArrayValue(value: items)
case let value as any Numeric:
return NumericValue(value: value)
case let value as String:
return StringValue(value: value)
case let fn as (String) throws -> Void:
return FunctionValue { args, _ in
var arg = ""
switch args[0].value {
case let value as String:
arg = value
case let value as Bool:
arg = String(value)
default:
throw JinjaError.runtime("Unknown arg type:\(type(of: args[0].value))")
case let value as Bool:
return BooleanValue(value: value)
case let values as [any Numeric]:
var items: [any RuntimeValue] = []
for value in values {
try items.append(self.convertToRuntimeValues(input: value))
}

try fn(arg)
return NullValue()
}
case let fn as (Bool) throws -> Void:
return FunctionValue { args, _ in
try fn(args[0].value as! Bool)
return ArrayValue(value: items)
case let value as any Numeric:
return NumericValue(value: value)
case let value as String:
return StringValue(value: value)
case let fn as (String) throws -> Void:
return FunctionValue { args, _ in
var arg = ""
switch args[0].value {
case let value as String:
arg = value
case let value as Bool:
arg = String(value)
default:
throw JinjaError.runtime("Unknown arg type:\(type(of: args[0].value))")
}
try fn(arg)
return NullValue()
}
case let fn as (Bool) throws -> Void:
return FunctionValue { args, _ in
try fn(args[0].value as! Bool)
return NullValue()
}
case let fn as (Int, Int?, Int) -> [Int]:
return FunctionValue { args, _ in
let result = fn(args[0].value as! Int, args[1].value as? Int, args[2].value as! Int)
return try self.convertToRuntimeValues(input: result)
}
case let values as [Any]:
var items: [any RuntimeValue] = []
for value in values {
try items.append(self.convertToRuntimeValues(input: value))
}
return ArrayValue(value: items)
case let dictionary as [String: String]:
var object: [String: any RuntimeValue] = [:]
for (key, value) in dictionary {
object[key] = StringValue(value: value)
}
return ObjectValue(value: object)
case let dictionary as [String: Any]:
var object: [String: any RuntimeValue] = [:]
for (key, value) in dictionary {
object[key] = try self.convertToRuntimeValues(input: value)
}
return ObjectValue(value: object)
case is NullValue:
return NullValue()
}
case let fn as (Int, Int?, Int) -> [Int]:
return FunctionValue { args, _ in
let result = fn(args[0].value as! Int, args[1].value as? Int, args[2].value as! Int)
return try self.convertToRuntimeValues(input: result)
}
case let values as [Any]:
var items: [any RuntimeValue] = []
for value in values {
try items.append(self.convertToRuntimeValues(input: value))
}
return ArrayValue(value: items)
case let dictionary as [String: String]:
var object: [String: any RuntimeValue] = [:]

for (key, value) in dictionary {
object[key] = StringValue(value: value)
}

return ObjectValue(value: object)
case is NullValue:
return NullValue()
default:
throw JinjaError.runtime("Cannot convert to runtime value: \(input) type:\(type(of: input))")
default:
throw JinjaError.runtime("Cannot convert to runtime value: \(input) type:\(type(of: input))")
}
}

Expand All @@ -181,9 +175,7 @@ class Environment {
if self.variables.contains(where: { $0.0 == name }) {
throw JinjaError.syntax("Variable already declared: \(name)")
}

self.variables[name] = value

return value
}

Expand All @@ -197,24 +189,20 @@ class Environment {
if self.variables.contains(where: { $0.0 == name }) {
return self
}

if let parent {
return try parent.resolve(name: name) as! Self
}

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

func lookupVariable(name: String) -> any RuntimeValue {
do {
if let value = try self.resolve(name: name).variables[name] {
return value
}
else {
} else {
return UndefinedValue()
}
}
catch {
} catch {
return UndefinedValue()
}
}
Expand Down
30 changes: 28 additions & 2 deletions Sources/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -452,15 +452,41 @@ func parse(tokens: [Token]) throws -> Program {

let iterable = try parseExpression()

let iterableExpr: Expression
if typeof(.if) {
current += 1 // consume if token
let predicate = try parseExpression()
iterableExpr = SelectExpression(iterable: iterable as! Expression, test: predicate as! Expression)
} else {
iterableExpr = iterable as! Expression
}

try expect(type: .closeStatement, error: "Expected closing statement token")

var body: [Statement] = []
while not(.openStatement, .endFor) {
var defaultBlock: [Statement] = []

while not(.openStatement, .endFor) && not(.openStatement, .else) {
try body.append(parseAny())
}

if typeof(.openStatement, .else) {
current += 1 // consume {%
try expect(type: .else, error: "Expected else token")
try expect(type: .closeStatement, error: "Expected closing statement token")

while not(.openStatement, .endFor) {
try defaultBlock.append(parseAny())
}
}

if let loopVariable = loopVariable as? Loopvar {
return For(loopvar: loopVariable, iterable: iterable as! Expression, body: body)
return For(
loopvar: loopVariable,
iterable: iterableExpr,
body: body,
defaultBlock: defaultBlock
)
}

throw JinjaError.syntax(
Expand Down
Loading

0 comments on commit b11a959

Please sign in to comment.