11const vm = require ( 'vm' )
2+ const merge = require ( 'lodash.merge' )
3+ const cloneDeep = require ( 'lodash.cloneDeep' )
24const parseAndReplace = require ( './expression_parser' )
35
4- let ctx , delimiters , unescapeDelimiters , delimiterRegex , unescapeDelimiterRegex , conditionals
6+ let delimiters , unescapeDelimiters , delimiterRegex , unescapeDelimiterRegex , conditionals , loops
57
68module . 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
2727function 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 ( / \s i n \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+ }
0 commit comments