mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-19 14:30:38 -07:00
feat: group_by/group_by_exp/find/find_exp from Jekyll, #443
This commit is contained in:
@@ -2,7 +2,7 @@ import { Drop } from '../drop/drop'
|
||||
import { __assign } from 'tslib'
|
||||
import { NormalizedFullOptions, defaultOptions, RenderOptions } from '../liquid-options'
|
||||
import { Scope } from './scope'
|
||||
import { isArray, isNil, isUndefined, isString, isFunction, toLiquid, InternalUndefinedVariableError, toValueSync } from '../util'
|
||||
import { isArray, isNil, isUndefined, isString, isFunction, toLiquid, InternalUndefinedVariableError, toValueSync, isObject } from '../util'
|
||||
|
||||
type PropertyKey = string | number;
|
||||
|
||||
@@ -37,7 +37,7 @@ export class Context {
|
||||
this.sync = !!renderOptions.sync
|
||||
this.opts = opts
|
||||
this.globals = renderOptions.globals ?? opts.globals
|
||||
this.environments = env
|
||||
this.environments = isObject(env) ? env : Object(env)
|
||||
this.strictVariables = renderOptions.strictVariables ?? this.opts.strictVariables
|
||||
this.ownPropertyOnly = renderOptions.ownPropertyOnly ?? opts.ownPropertyOnly
|
||||
}
|
||||
|
||||
+48
-7
@@ -1,8 +1,8 @@
|
||||
import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast, hasOwnProperty } from '../util'
|
||||
import { isTruthy } from '../render'
|
||||
import { FilterImpl } from '../template'
|
||||
import { Scope } from '../context'
|
||||
import { isComparable } from '../drop'
|
||||
import { equals, evalToken, isTruthy } from '../render'
|
||||
import { Value, FilterImpl } from '../template'
|
||||
import { Context, Scope } from '../context'
|
||||
import { Tokenizer } from '../parser'
|
||||
|
||||
export const join = argumentsToValue((v: any[], arg: string) => toArray(v).join(arg === undefined ? ' ' : arg))
|
||||
export const last = argumentsToValue((v: any) => isArray(v) ? arrayLast(v) : '')
|
||||
@@ -92,16 +92,57 @@ export function slice<T> (v: T[] | string, begin: number, length = 1): T[] | str
|
||||
export function * where<T extends object> (this: FilterImpl, arr: T[], property: string, expected?: any): IterableIterator<unknown> {
|
||||
const values: unknown[] = []
|
||||
arr = toArray(arr)
|
||||
const token = new Tokenizer(stringify(property)).readScopeValue()
|
||||
for (const item of arr) {
|
||||
values.push(yield this.context._getFromScope(item, stringify(property).split('.'), false))
|
||||
values.push(yield evalToken(token, new Context(item)))
|
||||
}
|
||||
return arr.filter((_, i) => {
|
||||
if (expected === undefined) return isTruthy(values[i], this.context)
|
||||
if (isComparable(expected)) return expected.equals(values[i])
|
||||
return values[i] === expected
|
||||
return equals(values[i], expected)
|
||||
})
|
||||
}
|
||||
|
||||
export function * group_by<T extends object> (arr: T[], property: string): IterableIterator<unknown> {
|
||||
const map = new Map()
|
||||
arr = toArray(arr)
|
||||
const token = new Tokenizer(stringify(property)).readScopeValue()
|
||||
for (const item of arr) {
|
||||
const key = yield evalToken(token, new Context(item))
|
||||
if (!map.has(key)) map.set(key, [])
|
||||
map.get(key).push(item)
|
||||
}
|
||||
return [...map.entries()].map(([name, items]) => ({ name, items }))
|
||||
}
|
||||
|
||||
export function * group_by_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
|
||||
const map = new Map()
|
||||
const keyTemplate = new Value(stringify(exp), this.liquid)
|
||||
for (const item of toArray(arr)) {
|
||||
const key = yield keyTemplate.value(new Context({ [itemName]: item }))
|
||||
if (!map.has(key)) map.set(key, [])
|
||||
map.get(key).push(item)
|
||||
}
|
||||
return [...map.entries()].map(([name, items]) => ({ name, items }))
|
||||
}
|
||||
|
||||
export function * find<T extends object> (this: FilterImpl, arr: T[], property: string, expected: string): IterableIterator<unknown> {
|
||||
const token = new Tokenizer(stringify(property)).readScopeValue()
|
||||
for (const item of toArray(arr)) {
|
||||
const value = yield evalToken(token, new Context(item))
|
||||
if (equals(value, expected)) return item
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function * find_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
|
||||
const predicate = new Value(stringify(exp), this.liquid)
|
||||
for (const item of toArray(arr)) {
|
||||
const value = yield predicate.value(new Context({ [itemName]: item }))
|
||||
if (value) return item
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function uniq<T> (arr: T[]): T[] {
|
||||
arr = toValue(arr)
|
||||
const u = {}
|
||||
|
||||
+28
-10
@@ -17,7 +17,7 @@ export class Tokenizer {
|
||||
public input: string,
|
||||
operators: Operators = defaultOptions.operators,
|
||||
public file?: string,
|
||||
private range?: [number, number]
|
||||
range?: [number, number]
|
||||
) {
|
||||
this.p = range ? range[0] : 0
|
||||
this.N = range ? range[1] : input.length
|
||||
@@ -249,6 +249,11 @@ export class Tokenizer {
|
||||
return new IdentifierToken(this.input, begin, this.p, this.file)
|
||||
}
|
||||
|
||||
readNonEmptyIdentifier (): IdentifierToken | undefined {
|
||||
const id = this.readIdentifier()
|
||||
return id.size() ? id : undefined
|
||||
}
|
||||
|
||||
readTagName (): string {
|
||||
this.skipBlank()
|
||||
// Handle inline comment tags
|
||||
@@ -269,8 +274,8 @@ export class Tokenizer {
|
||||
this.skipBlank()
|
||||
if (this.peek() === ',') ++this.p
|
||||
const begin = this.p
|
||||
const name = this.readIdentifier()
|
||||
if (!name.size()) return
|
||||
const name = this.readNonEmptyIdentifier()
|
||||
if (!name) return
|
||||
let value
|
||||
|
||||
this.skipBlank()
|
||||
@@ -306,6 +311,20 @@ export class Tokenizer {
|
||||
this.skipBlank()
|
||||
const begin = this.p
|
||||
const variable = this.readLiteral() || this.readQuoted() || this.readRange() || this.readNumber()
|
||||
const props = this.readProperties(!variable)
|
||||
if (!props.length) return variable
|
||||
return new PropertyAccessToken(variable, props, this.input, begin, this.p)
|
||||
}
|
||||
|
||||
readScopeValue (): ValueToken | undefined {
|
||||
this.skipBlank()
|
||||
const begin = this.p
|
||||
const props = this.readProperties()
|
||||
if (!props.length) return undefined
|
||||
return new PropertyAccessToken(undefined, props, this.input, begin, this.p)
|
||||
}
|
||||
|
||||
private readProperties (isBegin = true): (ValueToken | IdentifierToken)[] {
|
||||
const props: (ValueToken | IdentifierToken)[] = []
|
||||
while (true) {
|
||||
if (this.peek() === '[') {
|
||||
@@ -315,24 +334,23 @@ export class Tokenizer {
|
||||
props.push(prop)
|
||||
continue
|
||||
}
|
||||
if (!variable && !props.length) {
|
||||
const prop = this.readIdentifier()
|
||||
if (prop.size()) {
|
||||
if (isBegin && !props.length) {
|
||||
const prop = this.readNonEmptyIdentifier()
|
||||
if (prop) {
|
||||
props.push(prop)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (this.peek() === '.' && this.peek(1) !== '.') { // skip range syntax
|
||||
this.p++
|
||||
const prop = this.readIdentifier()
|
||||
if (!prop.size()) break
|
||||
const prop = this.readNonEmptyIdentifier()
|
||||
if (!prop) break
|
||||
props.push(prop)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if (!props.length) return variable
|
||||
return new PropertyAccessToken(variable, props, this.input, begin, this.p)
|
||||
return props
|
||||
}
|
||||
|
||||
readNumber (): NumberToken | undefined {
|
||||
|
||||
@@ -43,12 +43,12 @@ export function * evalToken (token: Token | undefined, ctx: Context, lenient = f
|
||||
|
||||
function * evalPropertyAccessToken (token: PropertyAccessToken, ctx: Context, lenient: boolean): IterableIterator<unknown> {
|
||||
const props: string[] = []
|
||||
const variable = yield evalToken(token.variable, ctx, lenient)
|
||||
for (const prop of token.props) {
|
||||
props.push((yield evalToken(prop, ctx, false)) as unknown as string)
|
||||
}
|
||||
try {
|
||||
if (token.variable) {
|
||||
const variable = yield evalToken(token.variable, ctx, lenient)
|
||||
return yield ctx._getFromScope(variable, props)
|
||||
} else {
|
||||
return yield ctx._get(props)
|
||||
|
||||
@@ -10,8 +10,8 @@ export type OperatorHandler = UnaryOperatorHandler | BinaryOperatorHandler;
|
||||
export type Operators = Record<string, OperatorHandler>
|
||||
|
||||
export const defaultOperators: Operators = {
|
||||
'==': equal,
|
||||
'!=': (l: any, r: any) => !equal(l, r),
|
||||
'==': equals,
|
||||
'!=': (l: any, r: any) => !equals(l, r),
|
||||
'>': (l: any, r: any) => {
|
||||
if (isComparable(l)) return l.gt(r)
|
||||
if (isComparable(r)) return r.lt(l)
|
||||
@@ -34,7 +34,7 @@ export const defaultOperators: Operators = {
|
||||
},
|
||||
'contains': (l: any, r: any) => {
|
||||
l = toValue(l)
|
||||
if (isArray(l)) return l.some((i) => equal(i, r))
|
||||
if (isArray(l)) return l.some((i) => equals(i, r))
|
||||
if (isFunction(l?.indexOf)) return l.indexOf(toValue(r)) > -1
|
||||
return false
|
||||
},
|
||||
@@ -43,18 +43,18 @@ export const defaultOperators: Operators = {
|
||||
'or': (l: any, r: any, ctx: Context) => isTruthy(toValue(l), ctx) || isTruthy(toValue(r), ctx)
|
||||
}
|
||||
|
||||
function equal (lhs: any, rhs: any): boolean {
|
||||
export function equals (lhs: any, rhs: any): boolean {
|
||||
if (isComparable(lhs)) return lhs.equals(rhs)
|
||||
if (isComparable(rhs)) return rhs.equals(lhs)
|
||||
lhs = toValue(lhs)
|
||||
rhs = toValue(rhs)
|
||||
if (isArray(lhs)) {
|
||||
return isArray(rhs) && arrayEqual(lhs, rhs)
|
||||
return isArray(rhs) && arrayEquals(lhs, rhs)
|
||||
}
|
||||
return lhs === rhs
|
||||
}
|
||||
|
||||
function arrayEqual (lhs: any[], rhs: any[]): boolean {
|
||||
function arrayEquals (lhs: any[], rhs: any[]): boolean {
|
||||
if (lhs.length !== rhs.length) return false
|
||||
return !lhs.some((value, i) => !equal(value, rhs[i]))
|
||||
return !lhs.some((value, i) => !equals(value, rhs[i]))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user