feat: group_by/group_by_exp/find/find_exp from Jekyll, #443

This commit is contained in:
Jun Yang
2024-04-14 19:34:07 +08:00
parent 258780e9a8
commit 2b713b721d
29 changed files with 775 additions and 35 deletions
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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)
+7 -7
View File
@@ -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]))
}