Skip to content

Commit 9ed7be3

Browse files
author
Jeff Escalante
committed
basic loop implementation
1 parent 9591be7 commit 9ed7be3

File tree

5 files changed

+126
-8
lines changed

5 files changed

+126
-8
lines changed

lib/index.js

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
const vm = require('vm')
2+
const merge = require('lodash.merge')
3+
const cloneDeep = require('lodash.cloneDeep')
24
const parseAndReplace = require('./expression_parser')
35

4-
let ctx, delimiters, unescapeDelimiters, delimiterRegex, unescapeDelimiterRegex, conditionals
6+
let delimiters, unescapeDelimiters, delimiterRegex, unescapeDelimiterRegex, conditionals, loops
57

68
module.exports = function PostHTMLExpressions (options = {}) {
7-
// the context in which expressions are evaluated
8-
ctx = vm.createContext(options.locals)
9-
109
// set up delimiter options and detection
1110
delimiters = options.delimiters || ['{{', '}}']
1211
unescapeDelimiters = options.unescapeDelimiters || ['{{{', '}}}']
@@ -19,12 +18,16 @@ module.exports = function PostHTMLExpressions (options = {}) {
1918

2019
// conditional and loop options
2120
conditionals = options.conditionalTags || ['if', 'elseif', 'else']
21+
loops = options.loopTags || ['each']
2222

2323
// kick off the parsing
2424
return walk.bind(null, options)
2525
}
2626

2727
function walk (opts, nodes) {
28+
// the context in which expressions are evaluated
29+
const ctx = vm.createContext(opts.locals)
30+
2831
// After a conditional has been resolved, we remove the conditional elements
2932
// from the tree. This variable determines how many to skip afterwards.
3033
let skip
@@ -55,8 +58,8 @@ function walk (opts, nodes) {
5558
}
5659
}
5760

58-
// if the node has content, recurse
59-
if (node.content) {
61+
// if the node has content, recurse (unless it's a loop, handled later)
62+
if (node.content && node.tag !== 'each') {
6063
node.content = walk(opts, node.content)
6164
}
6265

@@ -118,10 +121,48 @@ function walk (opts, nodes) {
118121
skip = current - i
119122
if (expResult) m.push(...expResult.content)
120123
return m
121-
// nodes.splice(i - 1, current - i, ...expResult)
122124
}
123125

124-
// return the modified node
126+
// parse loops
127+
if (node.tag === loops[0]) {
128+
if (!(node.attrs && node.attrs.loop)) {
129+
throw new Error(`the "${conditionals[1]}" tag must have a "loop" attribute`)
130+
}
131+
// parse the "loop" param
132+
const loopParams = parseLoopStatement(node.attrs.loop)
133+
const target = vm.runInContext(loopParams.expression, ctx)
134+
135+
if (typeof target !== 'object') {
136+
throw new Error('You must provide an array or object to loop through')
137+
}
138+
139+
if (loopParams.length < 1) {
140+
throw new Error('You must provide at least one loop argument')
141+
}
142+
143+
if (Array.isArray(target)) {
144+
for (let index = 0; index < target.length; index++) {
145+
const item = target[index]
146+
// add item and optional index loop locals
147+
const scopedLocals = {}
148+
scopedLocals[loopParams.keys[0]] = item
149+
if (loopParams.keys[1]) scopedLocals[loopParams.keys[1]] = index
150+
// merge nondestructively into existing locals
151+
const scopedOptions = merge(opts, { locals: scopedLocals })
152+
// provide the modified options to the content evaluation
153+
// we need to clone the node because the normal operation modifies
154+
// the node directly
155+
const content = cloneDeep(node.content)
156+
const res = walk(scopedOptions, content)
157+
m.push(res)
158+
}
159+
return m
160+
} else {
161+
// object loop
162+
}
163+
}
164+
165+
// return the node
125166
m.push(node)
126167
return m
127168
}, [])
@@ -139,3 +180,58 @@ function getNextTag (nodes, i, nodeCount) {
139180
}
140181
return [i, { tag: undefined }]
141182
}
183+
184+
function parseLoopStatement (input) {
185+
let current = 0
186+
let char = input[current]
187+
188+
// parse through keys `each **foo, bar** in x`, which is everything before
189+
// the word "in"
190+
const keys = []
191+
let key = ''
192+
while (!`${char}${lookahead(3)}`.match(/\sin\s/)) {
193+
key += char
194+
next()
195+
196+
// if we hit a comma, we're on to the next key
197+
if (char === ',') {
198+
keys.push(key.trim())
199+
key = ''
200+
next()
201+
}
202+
203+
// if we reach the end of the string without getting "in", it's an error
204+
if (typeof char === 'undefined') {
205+
throw new Error("Loop statement lacking 'in' keyword")
206+
}
207+
}
208+
keys.push(key.trim())
209+
210+
// Bypass the word " in", and ensure there's a space after
211+
next(4)
212+
213+
// the rest of the string is evaluated as the array/object to loop
214+
let expression = ''
215+
while (current < input.length) {
216+
expression += char
217+
next()
218+
}
219+
220+
return {keys, expression}
221+
222+
// Utility: Move to the next character in the parse
223+
function next (n = 1) {
224+
for (let i = 0; i < n; i++) { char = input[++current] }
225+
}
226+
227+
// Utility: looks ahead n characters and returns the result
228+
function lookahead (n) {
229+
let counter = current
230+
const target = current + n
231+
let res = ''
232+
while (counter < target) {
233+
res += input[++counter]
234+
}
235+
return res
236+
}
237+
}

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,9 @@
3030
},
3131
"scripts": {
3232
"test": "ava"
33+
},
34+
"dependencies": {
35+
"lodash.clonedeep": "^4.3.2",
36+
"lodash.merge": "^4.4.0"
3337
}
3438
}

test/fixtures/loop.expected.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<p>x</p>
2+
3+
<p>0: 1</p>
4+
5+
<p>1: 2</p>
6+
7+
<p>2: 3</p>
8+
9+
<p>x</p>

test/fixtures/loop.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<p>x</p>
2+
<each loop='item, index in items'>
3+
<p>{{index}}: {{item}}</p>
4+
</each>
5+
<p>x</p>

test/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ test('conditional - expression error', (t) => {
6767
})
6868
})
6969

70+
test('loop', (t) => {
71+
return matchExpected(t, 'loop', { locals: { items: [1, 2, 3] } })
72+
})
73+
7074
//
7175
// Utility
7276
//

0 commit comments

Comments
 (0)