Skip to content

Commit 8365aec

Browse files
committed
Add functionality from TypeScript implementation
1 parent 9d79438 commit 8365aec

File tree

5 files changed

+396
-4
lines changed

5 files changed

+396
-4
lines changed

Sources/Ast.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ struct For: Statement {
6767
var loopvar: Loopvar
6868
var iterable: Expression
6969
var body: [Statement]
70+
var defaultBlock: [Statement]
7071
}
7172

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

Sources/Parser.swift

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,15 +447,41 @@ func parse(tokens: [Token]) throws -> Program {
447447

448448
let iterable = try parseExpression()
449449

450+
let iterableExpr: Expression
451+
if typeof(.if) {
452+
current += 1 // consume if token
453+
let predicate = try parseExpression()
454+
iterableExpr = SelectExpression(iterable: iterable as! Expression, test: predicate as! Expression)
455+
} else {
456+
iterableExpr = iterable as! Expression
457+
}
458+
450459
try expect(type: .closeStatement, error: "Expected closing statement token")
451460

452461
var body: [Statement] = []
453-
while not(.openStatement, .endFor) {
462+
var defaultBlock: [Statement] = []
463+
464+
while not(.openStatement, .endFor) && not(.openStatement, .else) {
454465
try body.append(parseAny())
455466
}
456467

468+
if typeof(.openStatement, .else) {
469+
current += 1 // consume {%
470+
try expect(type: .else, error: "Expected else token")
471+
try expect(type: .closeStatement, error: "Expected closing statement token")
472+
473+
while not(.openStatement, .endFor) {
474+
try defaultBlock.append(parseAny())
475+
}
476+
}
477+
457478
if let loopVariable = loopVariable as? Loopvar {
458-
return For(loopvar: loopVariable, iterable: iterable as! Expression, body: body)
479+
return For(
480+
loopvar: loopVariable,
481+
iterable: iterableExpr,
482+
body: body,
483+
defaultBlock: defaultBlock
484+
)
459485
}
460486

461487
throw JinjaError.syntax(

Sources/Template.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ public struct Template {
2727
throw JinjaError.runtime("\(args)")
2828
}
2929
)
30-
try env.set(name: "range", value: range)
3130

3231
for (key, value) in items {
3332
try env.set(name: key, value: value)

Sources/Utilities.swift

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,86 @@ func slice<T>(_ array: [T], start: Int? = nil, stop: Int? = nil, step: Int? = 1)
3838

3939
return slicedArray
4040
}
41+
42+
func toJSON(_ input: any RuntimeValue, indent: Int? = nil, depth: Int = 0) throws -> String {
43+
let currentDepth = depth
44+
45+
switch input {
46+
case is NullValue, is UndefinedValue:
47+
return "null"
48+
49+
case let value as NumericValue:
50+
return String(describing: value.value)
51+
52+
case let value as StringValue:
53+
return "\"\(value.value)\"" // Directly wrap string in quotes
54+
55+
case let value as BooleanValue:
56+
return value.value ? "true" : "false"
57+
58+
case let arr as ArrayValue:
59+
let indentValue = indent != nil ? String(repeating: " ", count: indent!) : ""
60+
let basePadding = "\n" + String(repeating: indentValue, count: currentDepth)
61+
let childrenPadding = basePadding + indentValue // Depth + 1
62+
63+
let core = try arr.value.map { try toJSON($0, indent: indent, depth: currentDepth + 1) }
64+
65+
if indent != nil {
66+
return "[\(childrenPadding)\(core.joined(separator: ",\(childrenPadding)"))\(basePadding)]"
67+
} else {
68+
return "[\(core.joined(separator: ", "))]"
69+
}
70+
71+
case let obj as ObjectValue:
72+
let indentValue = indent != nil ? String(repeating: " ", count: indent!) : ""
73+
let basePadding = "\n" + String(repeating: indentValue, count: currentDepth)
74+
let childrenPadding = basePadding + indentValue // Depth + 1
75+
76+
let core = try obj.value.map { key, value in
77+
let v = "\"\(key)\": \(try toJSON(value, indent: indent, depth: currentDepth + 1))"
78+
return indent != nil ? "\(childrenPadding)\(v)" : v
79+
}
80+
81+
if indent != nil {
82+
return "{\(core.joined(separator: ","))\(basePadding)}"
83+
} else {
84+
return "{\(core.joined(separator: ", "))}"
85+
}
86+
87+
default:
88+
throw JinjaError.runtime("Cannot convert to JSON: \(type(of: input))")
89+
}
90+
}
91+
92+
// Helper function to convert values to JSON strings
93+
private func jsonString(_ value: Any) throws -> String {
94+
let data = try JSONSerialization.data(withJSONObject: value)
95+
guard let string = String(data: data, encoding: .utf8) else {
96+
throw JinjaError.runtime("Failed to convert value to JSON string")
97+
}
98+
return string
99+
}
100+
101+
extension String {
102+
func titleCase() -> String {
103+
self.components(separatedBy: .whitespacesAndNewlines)
104+
.map { $0.prefix(1).uppercased() + $0.dropFirst().lowercased() }
105+
.joined(separator: " ")
106+
}
107+
108+
func indent(_ width: Int, first: Bool = false, blank: Bool = false) -> String {
109+
let indent = String(repeating: " ", count: width)
110+
return self.components(separatedBy: .newlines)
111+
.enumerated()
112+
.map { index, line in
113+
if line.isEmpty && !blank {
114+
return line
115+
}
116+
if index == 0 && !first {
117+
return line
118+
}
119+
return indent + line
120+
}
121+
.joined(separator: "\n")
122+
}
123+
}

0 commit comments

Comments
 (0)