• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TotalTechGeek / json-logic-engine / 12020680110

25 Nov 2024 11:29PM UTC coverage: 92.114% (-0.2%) from 92.345%
12020680110

push

github

TotalTechGeek
Implement a tagged template system for compilation; reduce complexity all over the place, get rid of the need for 'useContext'

727 of 819 branches covered (88.77%)

Branch coverage included in aggregate %.

85 of 87 new or added lines in 5 files covered. (97.7%)

2 existing lines in 1 file now uncovered.

733 of 766 relevant lines covered (95.69%)

32101.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.62
/defaultMethods.js
1
// @ts-check
2
'use strict'
3

4
import asyncIterators from './async_iterators.js'
5
import { Sync, isSync } from './constants.js'
6
import declareSync from './utilities/declareSync.js'
7
import { build, buildString } from './compiler.js'
8
import chainingSupported from './utilities/chainingSupported.js'
9
import InvalidControlInput from './errors/InvalidControlInput.js'
10
import { splitPathMemoized } from './utilities/splitPath.js'
11

12
function isDeterministic (method, engine, buildState) {
13
  if (Array.isArray(method)) {
20,274✔
14
    return method.every((i) => isDeterministic(i, engine, buildState))
13,560✔
15
  }
16
  if (method && typeof method === 'object') {
15,918✔
17
    const func = Object.keys(method)[0]
3,708✔
18
    const lower = method[func]
3,708✔
19

20
    if (engine.isData(method, func)) return true
3,708✔
21
    if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
3,624!
22

23
    if (engine.methods[func].traverse === false) {
3,624!
24
      return typeof engine.methods[func].deterministic === 'function'
×
25
        ? engine.methods[func].deterministic(lower, buildState)
26
        : engine.methods[func].deterministic
27
    }
28
    return typeof engine.methods[func].deterministic === 'function'
3,624✔
29
      ? engine.methods[func].deterministic(lower, buildState)
30
      : engine.methods[func].deterministic &&
1,914✔
31
          isDeterministic(lower, engine, buildState)
32
  }
33
  return true
12,210✔
34
}
35

36
function isSyncDeep (method, engine, buildState) {
37
  if (Array.isArray(method)) {
20,382✔
38
    return method.every((i) => isSyncDeep(i, engine, buildState))
11,466✔
39
  }
40

41
  if (method && typeof method === 'object') {
16,284✔
42
    const func = Object.keys(method)[0]
5,988✔
43
    const lower = method[func]
5,988✔
44
    if (engine.isData(method, func)) return true
5,988✔
45
    if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`)
5,946!
46
    if (engine.methods[func].traverse === false) return typeof engine.methods[func][Sync] === 'function' ? engine.methods[func][Sync](lower, buildState) : engine.methods[func][Sync]
5,946!
47
    return typeof engine.methods[func][Sync] === 'function' ? engine.methods[func][Sync](lower, buildState) : engine.methods[func][Sync] && isSyncDeep(lower, engine, buildState)
3,486!
48
  }
49

50
  return true
10,296✔
51
}
52

53
const defaultMethods = {
54✔
54
  '+': (data) => {
55
    if (typeof data === 'string') return +data
482,052✔
56
    if (typeof data === 'number') return +data
481,860!
57
    let res = 0
481,860✔
58
    for (let i = 0; i < data.length; i++) res += +data[i]
963,888✔
59
    return res
481,860✔
60
  },
61
  '*': (data) => {
62
    let res = 1
864✔
63
    for (let i = 0; i < data.length; i++) res *= +data[i]
1,752✔
64
    return res
864✔
65
  },
66
  '/': (data) => {
67
    let res = data[0]
300✔
68
    for (let i = 1; i < data.length; i++) res /= +data[i]
324✔
69
    return res
300✔
70
  },
71
  '-': (data) => {
72
    if (typeof data === 'string') return -data
516✔
73
    if (typeof data === 'number') return -data
468✔
74
    if (data.length === 1) return -data[0]
396✔
75
    let res = data[0]
264✔
76
    for (let i = 1; i < data.length; i++) res -= +data[i]
288✔
77
    return res
264✔
78
  },
79
  '%': (data) => {
80
    let res = data[0]
504✔
81
    for (let i = 1; i < data.length; i++) res %= +data[i]
528✔
82
    return res
504✔
83
  },
84
  max: (data) => Math.max(...data),
336✔
85
  min: (data) => Math.min(...data),
336✔
86
  in: ([item, array]) => (array || []).includes(item),
456✔
87
  '>': ([a, b]) => a > b,
1,452✔
88
  '<': ([a, b, c]) => (c === undefined ? a < b : a < b && b < c),
1,680✔
89
  preserve: {
90
    traverse: false,
91
    method: declareSync((i) => i, true),
1,626✔
92
    [Sync]: () => true
462✔
93
  },
94
  if: {
95
    method: (input, context, above, engine) => {
96
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
3,186!
97

98
      if (input.length === 1) return (engine.fallback || engine).run(input[0], context, { above })
3,186✔
99
      if (input.length < 2) return null
2,958✔
100

101
      input = [...input]
2,874✔
102
      if (input.length % 2 !== 1) input.push(null)
2,874✔
103

104
      // fallback to the default if the condition is false
105
      const onFalse = input.pop()
2,874✔
106

107
      // while there are still conditions
108
      while (input.length) {
2,874✔
109
        const check = input.shift()
3,630✔
110
        const onTrue = input.shift()
3,630✔
111

112
        const test = (engine.fallback || engine).run(check, context, { above })
3,630✔
113

114
        // if the condition is true, run the true branch
115
        if (engine.truthy(test)) return (engine.fallback || engine).run(onTrue, context, { above })
3,630✔
116
      }
117

118
      return (engine.fallback || engine).run(onFalse, context, { above })
942✔
119
    },
120
    [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
3,564✔
121
    deterministic: (data, buildState) => {
122
      return isDeterministic(data, buildState.engine, buildState)
3,168✔
123
    },
124
    asyncMethod: async (input, context, above, engine) => {
125
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
102!
126

127
      // check the bounds
128
      if (input.length === 1) return engine.run(input[0], context, { above })
102✔
129
      if (input.length < 2) return null
66✔
130

131
      input = [...input]
54✔
132

133
      if (input.length % 2 !== 1) input.push(null)
54✔
134

135
      // fallback to the default if the condition is false
136
      const onFalse = input.pop()
54✔
137

138
      // while there are still conditions
139
      while (input.length) {
54✔
140
        const check = input.shift()
54✔
141
        const onTrue = input.shift()
54✔
142

143
        const test = await engine.run(check, context, { above })
54✔
144

145
        // if the condition is true, run the true branch
146
        if (engine.truthy(test)) return engine.run(onTrue, context, { above })
54✔
147
      }
148

149
      return engine.run(onFalse, context, { above })
18✔
150
    },
151
    traverse: false
152
  },
153
  '<=': ([a, b, c]) => (c === undefined ? a <= b : a <= b && b <= c),
696✔
154
  '>=': ([a, b]) => a >= b,
1,380✔
155
  // eslint-disable-next-line eqeqeq
156
  '==': ([a, b]) => a == b,
840✔
157
  '===': ([a, b]) => a === b,
648✔
158
  // eslint-disable-next-line eqeqeq
159
  '!=': ([a, b]) => a != b,
516✔
160
  '!==': ([a, b]) => a !== b,
576✔
161
  xor: ([a, b]) => a ^ b,
264✔
162
  or: (arr, _1, _2, engine) => {
163
    for (let i = 0; i < arr.length; i++) {
1,584✔
164
      if (engine.truthy(arr[i])) return arr[i]
2,556✔
165
    }
166
    return arr[arr.length - 1]
312✔
167
  },
168
  and: (arr, _1, _2, engine) => {
169
    for (let i = 0; i < arr.length; i++) {
2,112✔
170
      if (!engine.truthy(arr[i])) return arr[i]
3,564✔
171
    }
172
    return arr[arr.length - 1]
852✔
173
  },
174
  substr: ([string, from, end]) => {
175
    if (end < 0) {
720✔
176
      const result = string.substr(from)
192✔
177
      return result.substr(0, result.length + end)
192✔
178
    }
179
    return string.substr(from, end)
528✔
180
  },
181
  length: (i) => {
182
    if (typeof i === 'string' || Array.isArray(i)) return i.length
144✔
183
    if (i && typeof i === 'object') return Object.keys(i).length
96✔
184
    return 0
24✔
185
  },
186
  get: {
187
    method: ([data, key, defaultValue], context, above, engine) => {
188
      const notFound = defaultValue === undefined ? null : defaultValue
480✔
189

190
      const subProps = splitPathMemoized(String(key))
480✔
191
      for (let i = 0; i < subProps.length; i++) {
480✔
192
        if (data === null || data === undefined) {
480!
193
          return notFound
×
194
        }
195
        // Descending into context
196
        data = data[subProps[i]]
480✔
197
        if (data === undefined) {
480✔
198
          return notFound
48✔
199
        }
200
      }
201
      if (engine.allowFunctions || typeof data[key] !== 'function') {
432!
202
        return data
432✔
203
      }
204
    }
205
  },
206
  var: (key, context, above, engine) => {
207
    let b
208
    if (Array.isArray(key)) {
16,470✔
209
      b = key[1]
432✔
210
      key = key[0]
432✔
211
    }
212
    let iter = 0
16,470✔
213
    while (
16,470✔
214
      typeof key === 'string' &&
35,784✔
215
      key.startsWith('../') &&
216
      iter < above.length
217
    ) {
218
      context = above[iter++]
996✔
219
      key = key.substring(3)
996✔
220
    }
221

222
    const notFound = b === undefined ? null : b
16,470✔
223
    if (typeof key === 'undefined' || key === '' || key === null) {
16,470✔
224
      if (engine.allowFunctions || typeof context !== 'function') {
2,448!
225
        return context
2,448✔
226
      }
227
      return null
×
228
    }
229
    const subProps = splitPathMemoized(String(key))
14,022✔
230
    for (let i = 0; i < subProps.length; i++) {
14,022✔
231
      if (context === null || context === undefined) {
15,198✔
232
        return notFound
864✔
233
      }
234
      // Descending into context
235
      context = context[subProps[i]]
14,334✔
236
      if (context === undefined) {
14,334✔
237
        return notFound
2,376✔
238
      }
239
    }
240
    if (engine.allowFunctions || typeof context !== 'function') {
10,782✔
241
      return context
10,710✔
242
    }
243
    return null
72✔
244
  },
245
  missing: (checked, context, above, engine) => {
246
    return (Array.isArray(checked) ? checked : [checked]).filter((key) => {
2,736✔
247
      return defaultMethods.var(key, context, above, engine) === null
4,656✔
248
    })
249
  },
250
  missing_some: ([needCount, options], context, above, engine) => {
251
    const missing = defaultMethods.missing(options, context, above, engine)
864✔
252
    if (options.length - missing.length >= needCount) {
864✔
253
      return []
576✔
254
    } else {
255
      return missing
288✔
256
    }
257
  },
258
  map: createArrayIterativeMethod('map'),
259
  some: createArrayIterativeMethod('some', true),
260
  all: createArrayIterativeMethod('every', true),
261
  none: {
262
    traverse: false,
263
    // todo: add async build & build
264
    method: (val, context, above, engine) => {
265
      return !defaultMethods.some.method(val, context, above, engine)
192✔
266
    },
267
    asyncMethod: async (val, context, above, engine) => {
268
      return !(await defaultMethods.some.asyncMethod(
192✔
269
        val,
270
        context,
271
        above,
272
        engine
273
      ))
274
    },
275
    compile: (data, buildState) => {
276
      const result = defaultMethods.some.compile(data, buildState)
384✔
277
      return result ? buildState.compile`!(${result})` : false
384!
278
    }
279
  },
280
  merge: (arrays) => (Array.isArray(arrays) ? [].concat(...arrays) : [arrays]),
1,056✔
281
  every: createArrayIterativeMethod('every'),
282
  filter: createArrayIterativeMethod('filter'),
283
  reduce: {
284
    deterministic: (data, buildState) => {
285
      return (
372✔
286
        isDeterministic(data[0], buildState.engine, buildState) &&
432✔
287
        isDeterministic(data[1], buildState.engine, {
288
          ...buildState,
289
          insideIterator: true
290
        })
291
      )
292
    },
293
    compile: (data, buildState) => {
294
      if (!Array.isArray(data)) throw new InvalidControlInput(data)
288!
295
      const { async } = buildState
288✔
296
      let [selector, mapper, defaultValue] = data
288✔
297
      selector = buildString(selector, buildState)
288✔
298
      if (typeof defaultValue !== 'undefined') {
288!
299
        defaultValue = buildString(defaultValue, buildState)
288✔
300
      }
301
      const mapState = {
288✔
302
        ...buildState,
303
        extraArguments: 'above',
304
        avoidInlineAsync: true
305
      }
306
      mapper = build(mapper, mapState)
288✔
307
      buildState.methods.push(mapper)
288✔
308
      if (async) {
288✔
309
        if (!isSync(mapper) || selector.includes('await')) {
144!
310
          buildState.detectAsync = true
×
311
          if (typeof defaultValue !== 'undefined') {
×
312
            return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${
×
313
              buildState.methods.length - 1
314
            }]({ accumulator: a, current: b }, [null, context, ...above]), ${defaultValue})`
315
          }
316
          return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${
×
317
            buildState.methods.length - 1
318
          }]({ accumulator: a, current: b }, [null, context, ...above]))`
319
        }
320
      }
321
      if (typeof defaultValue !== 'undefined') {
288!
322
        return `(${selector} || []).reduce((a,b) => methods[${
288✔
323
          buildState.methods.length - 1
324
        }]({ accumulator: a, current: b }, [null, context, ...above]), ${defaultValue})`
325
      }
326
      return `(${selector} || []).reduce((a,b) => methods[${
×
327
        buildState.methods.length - 1
328
      }]({ accumulator: a, current: b }, [null, context, ...above]))`
329
    },
330
    method: (input, context, above, engine) => {
331
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
384✔
332
      let [selector, mapper, defaultValue] = input
360✔
333
      defaultValue = (engine.fallback || engine).run(defaultValue, context, {
360✔
334
        above
335
      })
336
      selector =
360✔
337
        (engine.fallback || engine).run(selector, context, {
960✔
338
          above
339
        }) || []
340
      const func = (accumulator, current) => {
360✔
341
        return (engine.fallback || engine).run(
1,218✔
342
          mapper,
343
          {
344
            accumulator,
345
            current
346
          },
347
          {
348
            above: [selector, context, ...above]
349
          }
350
        )
351
      }
352
      if (typeof defaultValue === 'undefined') {
360✔
353
        return selector.reduce(func)
54✔
354
      }
355
      return selector.reduce(func, defaultValue)
306✔
356
    },
357
    [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
360✔
358
    asyncMethod: async (input, context, above, engine) => {
359
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
24!
360
      let [selector, mapper, defaultValue] = input
24✔
361
      defaultValue = await engine.run(defaultValue, context, {
24✔
362
        above
363
      })
364
      selector =
24✔
365
        (await engine.run(selector, context, {
24!
366
          above
367
        })) || []
368
      return asyncIterators.reduce(
24✔
369
        selector,
370
        (accumulator, current) => {
371
          return engine.run(
102✔
372
            mapper,
373
            {
374
              accumulator,
375
              current
376
            },
377
            {
378
              above: [selector, context, ...above]
379
            }
380
          )
381
        },
382
        defaultValue
383
      )
384
    },
385
    traverse: false
386
  },
387
  '!': (value, _1, _2, engine) => Array.isArray(value) ? !engine.truthy(value[0]) : !engine.truthy(value),
876✔
388
  '!!': (value, _1, _2, engine) => Boolean(Array.isArray(value) ? engine.truthy(value[0]) : engine.truthy(value)),
396✔
389
  cat: (arr) => {
390
    if (typeof arr === 'string') return arr
528✔
391
    let res = ''
468✔
392
    for (let i = 0; i < arr.length; i++) res += arr[i]
1,032✔
393
    return res
468✔
394
  },
395
  keys: (obj) => typeof obj === 'object' ? Object.keys(obj) : [],
48✔
396
  eachKey: {
397
    traverse: false,
398
    [Sync]: (data, buildState) => isSyncDeep(Object.values(data[Object.keys(data)[0]]), buildState.engine, buildState),
30✔
399
    method: (object, context, above, engine) => {
400
      const result = Object.keys(object).reduce((accumulator, key) => {
78✔
401
        const item = object[key]
138✔
402
        Object.defineProperty(accumulator, key, {
138✔
403
          enumerable: true,
404
          value: (engine.fallback || engine).run(item, context, { above })
246✔
405
        })
406
        return accumulator
138✔
407
      }, {})
408
      return result
78✔
409
    },
410
    deterministic: (data, buildState) => {
411
      if (data && typeof data === 'object') {
216✔
412
        return Object.values(data).every((i) => {
192✔
413
          return isDeterministic(i, buildState.engine, buildState)
252✔
414
        })
415
      }
416
      throw new InvalidControlInput(data)
24✔
417
    },
418
    compile: (data, buildState) => {
419
      // what's nice about this is that I don't have to worry about whether it's async or not, the lower entries take care of that ;)
420
      // however, this is not engineered support yields, I will have to make a note of that & possibly support it at a later point.
421
      if (data && typeof data === 'object') {
192✔
422
        const result = `({ ${Object.keys(data)
168✔
423
          .reduce((accumulator, key) => {
424
            accumulator.push(
240✔
425
              // @ts-ignore Never[] is not accurate
426
              `${JSON.stringify(key)}: ${buildString(data[key], buildState)}`
427
            )
428
            return accumulator
240✔
429
          }, [])
430
          .join(',')} })`
431
        return result
168✔
432
      }
433
      throw new InvalidControlInput(data)
24✔
434
    },
435
    asyncMethod: async (object, context, above, engine) => {
436
      const result = await asyncIterators.reduce(
18✔
437
        Object.keys(object),
438
        async (accumulator, key) => {
439
          const item = object[key]
30✔
440
          Object.defineProperty(accumulator, key, {
30✔
441
            enumerable: true,
442
            value: await engine.run(item, context, { above })
443
          })
444
          return accumulator
30✔
445
        },
446
        {}
447
      )
448
      return result
18✔
449
    }
450
  }
451
}
452

453
function createArrayIterativeMethod (name, useTruthy = false) {
162✔
454
  return {
270✔
455
    deterministic: (data, buildState) => {
456
      return (
1,722✔
457
        isDeterministic(data[0], buildState.engine, buildState) &&
1,956✔
458
        isDeterministic(data[1], buildState.engine, {
459
          ...buildState,
460
          insideIterator: true
461
        })
462
      )
463
    },
464
    [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
900✔
465
    method: (input, context, above, engine) => {
466
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
1,422✔
467
      let [selector, mapper] = input
1,398✔
468
      selector =
1,398✔
469
        (engine.fallback || engine).run(selector, context, {
3,828✔
470
          above
471
        }) || []
472

473
      return selector[name]((i, index) => {
1,398✔
474
        const result = (engine.fallback || engine).run(mapper, i, {
2,694✔
475
          above: [{ item: selector, index }, context, ...above]
476
        })
477
        return useTruthy ? engine.truthy(result) : result
2,694✔
478
      })
479
    },
480
    asyncMethod: async (input, context, above, engine) => {
481
      if (!Array.isArray(input)) throw new InvalidControlInput(input)
534!
482
      let [selector, mapper] = input
534✔
483
      selector =
534✔
484
        (await engine.run(selector, context, {
534!
485
          above
486
        })) || []
487
      return asyncIterators[name](selector, (i, index) => {
534✔
488
        const result = engine.run(mapper, i, {
882✔
489
          above: [{ item: selector, index }, context, ...above]
490
        })
491
        return useTruthy ? engine.truthy(result) : result
882✔
492
      })
493
    },
494
    compile: (data, buildState) => {
495
      if (!Array.isArray(data)) throw new InvalidControlInput(data)
1,158!
496
      const { async } = buildState
1,158✔
497
      const [selector, mapper] = data
1,158✔
498

499
      const mapState = {
1,158✔
500
        ...buildState,
501
        avoidInlineAsync: true,
502
        iteratorCompile: true,
503
        extraArguments: 'index, above'
504
      }
505

506
      if (async) {
1,158✔
507
        if (!isSyncDeep(mapper, buildState.engine, buildState)) {
582✔
508
          buildState.detectAsync = true
6✔
509
          return buildState.compile`await asyncIterators[${name}](${selector} || [], (i, x) => ${build(mapper, mapState)}(i, x, [{ item: null }, context, ...above]))`
6✔
510
        }
511
      }
512

513
      return buildState.compile`(${selector} || [])[${name}]((i, x) => ${build(mapper, mapState)}(i, x, [{ item: null }, context, ...above]))`
1,152✔
514
    },
515
    traverse: false
516
  }
517
}
518
defaultMethods['?:'] = defaultMethods.if
54✔
519
// declare all of the functions here synchronous
520
Object.keys(defaultMethods).forEach((item) => {
54✔
521
  if (typeof defaultMethods[item] === 'function') {
2,214✔
522
    defaultMethods[item][Sync] = true
1,566✔
523
  }
524
  defaultMethods[item].deterministic =
2,214✔
525
    typeof defaultMethods[item].deterministic === 'undefined'
2,214✔
526
      ? true
527
      : defaultMethods[item].deterministic
528
})
529
// @ts-ignore Allow custom attribute
530
defaultMethods.var.deterministic = (data, buildState) => {
54✔
531
  return buildState.insideIterator && !String(data).includes('../../')
11,748✔
532
}
533
Object.assign(defaultMethods.missing, {
54✔
534
  deterministic: false
535
})
536
Object.assign(defaultMethods.missing_some, {
54✔
537
  deterministic: false
538
})
539
// @ts-ignore Allow custom attribute
540
defaultMethods['<'].compile = function (data, buildState) {
54✔
541
  if (!Array.isArray(data)) return false
600✔
542
  if (data.length === 2) return buildState.compile`(${data[0]} < ${data[1]})`
576✔
543
  if (data.length === 3) return buildState.compile`(${data[0]} < ${data[1]} && ${data[1]} < ${data[2]})`
72!
UNCOV
544
  return false
×
545
}
546
// @ts-ignore Allow custom attribute
547
defaultMethods['<='].compile = function (data, buildState) {
54✔
548
  if (!Array.isArray(data)) return false
168✔
549
  if (data.length === 2) return buildState.compile`(${data[0]} <= ${data[1]})`
144✔
550
  if (data.length === 3) return buildState.compile`(${data[0]} <= ${data[1]} && ${data[1]} <= ${data[2]})`
48!
UNCOV
551
  return false
×
552
}
553
// @ts-ignore Allow custom attribute
554
defaultMethods.min.compile = function (data, buildState) {
54✔
555
  if (!Array.isArray(data)) return false
120✔
556
  return `Math.min(${data
96✔
557
    .map((i) => buildString(i, buildState))
240✔
558
    .join(', ')})`
559
}
560
// @ts-ignore Allow custom attribute
561
defaultMethods.max.compile = function (data, buildState) {
54✔
562
  if (!Array.isArray(data)) return false
120✔
563
  return `Math.max(${data
96✔
564
    .map((i) => buildString(i, buildState))
240✔
565
    .join(', ')})`
566
}
567
// @ts-ignore Allow custom attribute
568
defaultMethods['>'].compile = function (data, buildState) {
54✔
569
  if (!Array.isArray(data)) return false
384✔
570
  if (data.length !== 2) return false
360!
571
  return buildState.compile`(${data[0]} > ${data[1]})`
360✔
572
}
573
// @ts-ignore Allow custom attribute
574
defaultMethods['>='].compile = function (data, buildState) {
54✔
575
  if (!Array.isArray(data)) return false
456✔
576
  if (data.length !== 2) return false
432!
577
  return buildState.compile`(${data[0]} >= ${data[1]})`
432✔
578
}
579
// @ts-ignore Allow custom attribute
580
defaultMethods['=='].compile = function (data, buildState) {
54✔
581
  if (!Array.isArray(data)) return false
240✔
582
  if (data.length !== 2) return false
216!
583
  return buildState.compile`(${data[0]} == ${data[1]})`
216✔
584
}
585
// @ts-ignore Allow custom attribute
586
defaultMethods['!='].compile = function (data, buildState) {
54✔
587
  if (!Array.isArray(data)) return false
96✔
588
  if (data.length !== 2) return false
72!
589
  return buildState.compile`(${data[0]} != ${data[1]})`
72✔
590
}
591
// @ts-ignore Allow custom attribute
592
defaultMethods.if.compile = function (data, buildState) {
54✔
593
  if (!Array.isArray(data)) return false
1,368!
594
  if (data.length < 3) return false
1,368✔
595

596
  data = [...data]
1,224✔
597
  if (data.length % 2 !== 1) data.push(null)
1,224✔
598
  const onFalse = data.pop()
1,224✔
599

600
  let res = buildState.compile``
1,224✔
601
  while (data.length) {
1,224✔
602
    const condition = data.shift()
1,848✔
603
    const onTrue = data.shift()
1,848✔
604
    res = buildState.compile`${res} engine.truthy(${condition}) ? ${onTrue} : `
1,848✔
605
  }
606

607
  return buildState.compile`(${res} ${onFalse})`
1,224✔
608
}
609
// @ts-ignore Allow custom attribute
610
defaultMethods['==='].compile = function (data, buildState) {
54✔
611
  if (!Array.isArray(data)) return false
144✔
612
  if (data.length !== 2) return false
120!
613
  return buildState.compile`(${data[0]} === ${data[1]})`
120✔
614
}
615
// @ts-ignore Allow custom attribute
616
defaultMethods['+'].compile = function (data, buildState) {
54✔
617
  if (Array.isArray(data)) {
546✔
618
    return `(${data
450✔
619
      .map((i) => `(+${buildString(i, buildState)})`)
1,026✔
620
      .join(' + ')})`
621
  } else if (typeof data === 'string' || typeof data === 'number') {
96✔
622
    return `(+${buildString(data, buildState)})`
72✔
623
  } else {
624
    return `([].concat(${buildString(
24✔
625
      data,
626
      buildState
627
    )})).reduce((a,b) => (+a)+(+b), 0)`
628
  }
629
}
630

631
// @ts-ignore Allow custom attribute
632
defaultMethods['%'].compile = function (data, buildState) {
54✔
633
  if (Array.isArray(data)) {
168✔
634
    return `(${data
144✔
635
      .map((i) => `(+${buildString(i, buildState)})`)
288✔
636
      .join(' % ')})`
637
  } else {
638
    return `(${buildString(data, buildState)}).reduce((a,b) => (+a)%(+b))`
24✔
639
  }
640
}
641

642
// @ts-ignore Allow custom attribute
643
defaultMethods.or.compile = function (data, buildState) {
54✔
644
  if (!buildState.engine.truthy.IDENTITY) return false
384✔
645
  if (Array.isArray(data)) {
24!
646
    return `(${data.map((i) => buildString(i, buildState)).join(' || ')})`
×
647
  } else {
648
    return `(${buildString(data, buildState)}).reduce((a,b) => a||b, false)`
24✔
649
  }
650
}
651

652
// @ts-ignore Allow custom attribute
653
defaultMethods.in.compile = function (data, buildState) {
54✔
654
  if (!Array.isArray(data)) return false
216!
655
  return buildState.compile`(${data[1]} || []).includes(${data[0]})`
216✔
656
}
657

658
// @ts-ignore Allow custom attribute
659
defaultMethods.and.compile = function (data, buildState) {
54✔
660
  if (!buildState.engine.truthy.IDENTITY) return false
528✔
661
  if (Array.isArray(data)) {
24!
662
    return `(${data.map((i) => buildString(i, buildState)).join(' && ')})`
×
663
  } else {
664
    return `(${buildString(data, buildState)}).reduce((a,b) => a&&b, true)`
24✔
665
  }
666
}
667

668
// @ts-ignore Allow custom attribute
669
defaultMethods['-'].compile = function (data, buildState) {
54✔
670
  if (Array.isArray(data)) {
216✔
671
    return `${data.length === 1 ? '-' : ''}(${data
144✔
672
      .map((i) => `(+${buildString(i, buildState)})`)
216✔
673
      .join(' - ')})`
674
  }
675
  if (typeof data === 'string' || typeof data === 'number') {
72✔
676
    return `(-${buildString(data, buildState)})`
48✔
677
  } else {
678
    return `((a=>(a.length===1?a[0]=-a[0]:a)&0||a)([].concat(${buildString(
24✔
679
      data,
680
      buildState
681
    )}))).reduce((a,b) => (+a)-(+b))`
682
  }
683
}
684
// @ts-ignore Allow custom attribute
685
defaultMethods['/'].compile = function (data, buildState) {
54✔
686
  if (Array.isArray(data)) {
120✔
687
    return `(${data
96✔
688
      .map((i) => `(+${buildString(i, buildState)})`)
192✔
689
      .join(' / ')})`
690
  } else {
691
    return `(${buildString(data, buildState)}).reduce((a,b) => (+a)/(+b))`
24✔
692
  }
693
}
694
// @ts-ignore Allow custom attribute
695
defaultMethods['*'].compile = function (data, buildState) {
54✔
696
  if (Array.isArray(data)) {
336✔
697
    return `(${data
312✔
698
      .map((i) => `(+${buildString(i, buildState)})`)
624✔
699
      .join(' * ')})`
700
  } else {
701
    return `(${buildString(data, buildState)}).reduce((a,b) => (+a)*(+b))`
24✔
702
  }
703
}
704
// @ts-ignore Allow custom attribute
705
defaultMethods.cat.compile = function (data, buildState) {
54✔
706
  if (typeof data === 'string') return JSON.stringify(data)
264✔
707
  if (!Array.isArray(data)) return false
240✔
708
  let res = buildState.compile`''`
216✔
709
  for (let i = 0; i < data.length; i++) res = buildState.compile`${res} + ${data[i]}`
480✔
710
  return buildState.compile`(${res})`
216✔
711
}
712

713
// @ts-ignore Allow custom attribute
714
defaultMethods['!'].compile = function (
54✔
715
  data,
716
  buildState
717
) {
718
  if (Array.isArray(data)) return buildState.compile`(!engine.truthy(${data[0]}))`
288✔
719
  return buildState.compile`(!engine.truthy(${data}))`
120✔
720
}
721

722
defaultMethods.not = defaultMethods['!']
54✔
723

724
// @ts-ignore Allow custom attribute
725
defaultMethods['!!'].compile = function (data, buildState) {
54✔
726
  if (Array.isArray(data)) return buildState.compile`(!!engine.truthy(${data[0]}))`
120✔
727
  return `(!!engine.truthy(${data}))`
24✔
728
}
729
defaultMethods.none.deterministic = defaultMethods.some.deterministic
54✔
730
defaultMethods.get.compile = function (data, buildState) {
54✔
731
  let defaultValue = null
396✔
732
  let key = data
396✔
733
  let obj = null
396✔
734
  if (Array.isArray(data) && data.length <= 3) {
396!
735
    obj = data[0]
396✔
736
    key = data[1]
396✔
737
    defaultValue = typeof data[2] === 'undefined' ? null : data[2]
396✔
738

739
    // Bail out if the key is dynamic; dynamic keys are not really optimized by this block.
740
    if (key && typeof key === 'object') return false
396✔
741

742
    key = key.toString()
288✔
743
    const pieces = splitPathMemoized(key)
288✔
744
    if (!chainingSupported) {
288✔
745
      return `(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce(
48✔
746
        (text, i) => {
747
          return `(${text}||0)[${JSON.stringify(i)}]`
48✔
748
        },
749
        `(${buildString(obj, buildState)}||0)`
750
      )}, ${buildString(defaultValue, buildState)}))`
751
    }
752
    return `((${buildString(obj, buildState)})${pieces
240✔
753
      .map((i) => `?.[${buildString(i, buildState)}]`)
240✔
754
      .join('')} ?? ${buildString(defaultValue, buildState)})`
755
  }
756
  return false
×
757
}
758
// @ts-ignore Allow custom attribute
759
defaultMethods.var.compile = function (data, buildState) {
54✔
760
  let key = data
5,514✔
761
  let defaultValue = null
5,514✔
762
  buildState.varTop = buildState.varTop || new Set()
5,514✔
763
  if (
5,514!
764
    !key ||
11,490✔
765
    typeof data === 'string' ||
766
    typeof data === 'number' ||
767
    (Array.isArray(data) && data.length <= 2)
768
  ) {
769
    if (data === '../index' && buildState.iteratorCompile) return 'index'
5,514!
770

771
    if (Array.isArray(data)) {
5,514✔
772
      key = data[0]
384✔
773
      defaultValue = typeof data[1] === 'undefined' ? null : data[1]
384✔
774
    }
775

776
    // this counts the number of var accesses to determine if they're all just using this override.
777
    // this allows for a small optimization :)
778
    if (typeof key === 'undefined' || key === null || key === '') return 'context'
5,514✔
779
    if (typeof key !== 'string' && typeof key !== 'number') return false
4,728✔
780

781
    key = key.toString()
4,680✔
782
    if (key.includes('../')) return false
4,680✔
783

784
    const pieces = splitPathMemoized(key)
4,626✔
785
    const [top] = pieces
4,626✔
786
    buildState.varTop.add(top)
4,626✔
787

788
    if (!buildState.engine.allowFunctions) buildState.methods.preventFunctions = a => typeof a === 'function' ? null : a
6,036✔
789
    else buildState.methods.preventFunctions = a => a
48✔
790

791
    // support older versions of node
792
    if (!chainingSupported) {
4,626✔
793
      return `(methods.preventFunctions(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce(
771✔
NEW
794
        (text, i) => `(${text}||0)[${JSON.stringify(i)}]`,
867✔
795
        '(context||0)'
796
      )}, ${buildString(defaultValue, buildState)})))`
797
    }
798
    return `(methods.preventFunctions(context${pieces
3,855✔
799
      .map((i) => `?.[${JSON.stringify(i)}]`)
4,335✔
800
      .join('')} ?? ${buildString(defaultValue, buildState)}))`
801
  }
802
  return false
×
803
}
804

805
export default {
806
  ...defaultMethods
807
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc