mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 20:30:39 -07:00
@@ -6,12 +6,15 @@ title: date
|
|||||||
Date filter is used to convert a timestamp into the specified format.
|
Date filter is used to convert a timestamp into the specified format.
|
||||||
|
|
||||||
* LiquidJS tries to conform to Shopify/Liquid, which uses Ruby's core [Time#strftime(string)](https://www.ruby-doc.org/core/Time.html#method-i-strftime). There're differences with [Ruby's format flags](https://ruby-doc.org/core/strftime_formatting_rdoc.html):
|
* LiquidJS tries to conform to Shopify/Liquid, which uses Ruby's core [Time#strftime(string)](https://www.ruby-doc.org/core/Time.html#method-i-strftime). There're differences with [Ruby's format flags](https://ruby-doc.org/core/strftime_formatting_rdoc.html):
|
||||||
* `%Z` (since v10.11.1) works when there's a passed-in timezone name from `LiquidOption` or in-place value (see TimeZone below). If passed-in timezone is an offset number instead of string, it'll behave like `%z`. If there's none passed-in timezone, it returns [the runtime's default time zone](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#timezone).
|
* `%Z` (since v10.11.1) is replaced by the passed-in timezone name from `LiquidOption` or in-place value (see TimeZone below). If passed-in timezone is an offset number instead of string, it'll behave like `%z`. If there's none passed-in timezone, it returns [the runtime's default time zone](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions#timezone).
|
||||||
* LiquidJS provides an additional `%q` flag for date ordinals. e.g. `{{ '2023/02/02' | date: '%d%q of %b'}}` => `02nd of Feb`
|
* LiquidJS provides an additional `%q` flag for date ordinals. e.g. `{{ '2023/02/02' | date: '%d%q of %b'}}` => `02nd of Feb`
|
||||||
* Date literals are firstly converted to `Date` object via [new Date()][jsDate], that means literal values are considered in runtime's time zone by default.
|
* Date literals are firstly converted to `Date` object via [new Date()][jsDate], that means literal values are considered in runtime's time zone by default.
|
||||||
* The format filter argument is optional:
|
* The format filter argument is optional:
|
||||||
* If not provided, it defaults to `%A, %B %-e, %Y at %-l:%M %P %z`.
|
* If not provided, it defaults to `%A, %B %-e, %Y at %-l:%M %P %z`.
|
||||||
* The above default can be overridden by [`dateFormat`](/api/interfaces/LiquidOptions.html#dateFormat) LiquidJS option.
|
* The above default can be overridden by [`dateFormat`](/api/interfaces/LiquidOptions.html#dateFormat) LiquidJS option.
|
||||||
|
* LiquidJS `date` supports locale specific weekdays and month names, which will fallback to English where `Intl` is not supported.
|
||||||
|
* Ordinals (`%q`) and Jekyll specific date filters are English-only.
|
||||||
|
* [`locale`](/api/interfaces/LiquidOptions.html#locale) can be set when creating Liquid instance. Defaults to `Intl.DateTimeFormat().resolvedOptions.locale`).
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
```liquid
|
```liquid
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class Context {
|
|||||||
this.strictVariables = renderOptions.strictVariables ?? this.opts.strictVariables
|
this.strictVariables = renderOptions.strictVariables ?? this.opts.strictVariables
|
||||||
this.ownPropertyOnly = renderOptions.ownPropertyOnly ?? opts.ownPropertyOnly
|
this.ownPropertyOnly = renderOptions.ownPropertyOnly ?? opts.ownPropertyOnly
|
||||||
this.memoryLimit = memoryLimit ?? new Limiter('memory alloc', renderOptions.memoryLimit ?? opts.memoryLimit)
|
this.memoryLimit = memoryLimit ?? new Limiter('memory alloc', renderOptions.memoryLimit ?? opts.memoryLimit)
|
||||||
this.renderLimit = renderLimit ?? new Limiter('template render', performance.now() + (renderOptions.templateLimit ?? opts.renderLimit))
|
this.renderLimit = renderLimit ?? new Limiter('template render', performance.now() + (renderOptions.renderLimit ?? opts.renderLimit))
|
||||||
}
|
}
|
||||||
public getRegister (key: string) {
|
public getRegister (key: string) {
|
||||||
return (this.registers[key] = this.registers[key] || {})
|
return (this.registers[key] = this.registers[key] || {})
|
||||||
|
|||||||
@@ -175,7 +175,6 @@ export function * find<T extends object> (this: FilterImpl, arr: T[], property:
|
|||||||
const value = yield evalToken(token, this.context.spawn(item))
|
const value = yield evalToken(token, this.context.spawn(item))
|
||||||
if (equals(value, expected)) return 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> {
|
export function * find_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
|
||||||
@@ -185,7 +184,6 @@ export function * find_exp<T extends object> (this: FilterImpl, arr: T[], itemNa
|
|||||||
const value = yield predicate.value(this.context.spawn({ [itemName]: item }))
|
const value = yield predicate.value(this.context.spawn({ [itemName]: item }))
|
||||||
if (value) return item
|
if (value) return item
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uniq<T> (this: FilterImpl, arr: T[]): T[] {
|
export function uniq<T> (this: FilterImpl, arr: T[]): T[] {
|
||||||
|
|||||||
+14
-22
@@ -1,6 +1,6 @@
|
|||||||
import { toValue, stringify, isString, isNumber, TimezoneDate, LiquidDate, strftime, isNil } from '../util'
|
import { toValue, stringify, isString, isNumber, LiquidDate, strftime, isNil } from '../util'
|
||||||
import { FilterImpl } from '../template'
|
import { FilterImpl } from '../template'
|
||||||
import { LiquidOptions } from '../liquid-options'
|
import { NormalizedFullOptions } from '../liquid-options'
|
||||||
|
|
||||||
export function date (this: FilterImpl, v: string | Date, format?: string, timezoneOffset?: number | string) {
|
export function date (this: FilterImpl, v: string | Date, format?: string, timezoneOffset?: number | string) {
|
||||||
const size = ((v as string)?.length ?? 0) + (format?.length ?? 0) + ((timezoneOffset as string)?.length ?? 0)
|
const size = ((v as string)?.length ?? 0) + (format?.length ?? 0) + ((timezoneOffset as string)?.length ?? 0)
|
||||||
@@ -40,33 +40,25 @@ function stringify_date (this: FilterImpl, v: string | Date, month_type: string,
|
|||||||
return strftime(date, `%d ${month_type} %Y`)
|
return strftime(date, `%d ${month_type} %Y`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDate (v: string | Date, opts: LiquidOptions, timezoneOffset?: number | string): LiquidDate | undefined {
|
function parseDate (v: string | Date, opts: NormalizedFullOptions, timezoneOffset?: number | string): LiquidDate | undefined {
|
||||||
let date: LiquidDate
|
let date: LiquidDate | undefined
|
||||||
|
const defaultTimezoneOffset = timezoneOffset ?? opts.timezoneOffset
|
||||||
|
const locale = opts.locale
|
||||||
v = toValue(v)
|
v = toValue(v)
|
||||||
if (v === 'now' || v === 'today') {
|
if (v === 'now' || v === 'today') {
|
||||||
date = new Date()
|
date = new LiquidDate(Date.now(), locale, defaultTimezoneOffset)
|
||||||
} else if (isNumber(v)) {
|
} else if (isNumber(v)) {
|
||||||
date = new Date(v * 1000)
|
date = new LiquidDate(v * 1000, locale, defaultTimezoneOffset)
|
||||||
} else if (isString(v)) {
|
} else if (isString(v)) {
|
||||||
if (/^\d+$/.test(v)) {
|
if (/^\d+$/.test(v)) {
|
||||||
date = new Date(+v * 1000)
|
date = new LiquidDate(+v * 1000, locale, defaultTimezoneOffset)
|
||||||
} else if (opts.preserveTimezones) {
|
} else if (opts.preserveTimezones && timezoneOffset === undefined) {
|
||||||
date = TimezoneDate.createDateFixedToTimezone(v)
|
date = LiquidDate.createDateFixedToTimezone(v, locale)
|
||||||
} else {
|
} else {
|
||||||
date = new Date(v)
|
date = new LiquidDate(v, locale, defaultTimezoneOffset)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
date = v
|
date = new LiquidDate(v, locale, defaultTimezoneOffset)
|
||||||
}
|
}
|
||||||
if (!isValidDate(date)) return
|
return date.valid() ? date : undefined
|
||||||
if (timezoneOffset !== undefined) {
|
|
||||||
date = new TimezoneDate(date, timezoneOffset)
|
|
||||||
} else if (!(date instanceof TimezoneDate) && opts.timezoneOffset !== undefined) {
|
|
||||||
date = new TimezoneDate(date, opts.timezoneOffset)
|
|
||||||
}
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidDate (date: any): date is Date {
|
|
||||||
return (date instanceof Date || date instanceof TimezoneDate) && !isNaN(date.getTime())
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ export class Loader {
|
|||||||
const rRelativePath = new RegExp(['.' + sep, '..' + sep, './', '../'].map(prefix => escapeRegex(prefix)).join('|'))
|
const rRelativePath = new RegExp(['.' + sep, '..' + sep, './', '../'].map(prefix => escapeRegex(prefix)).join('|'))
|
||||||
this.shouldLoadRelative = (referencedFile: string) => rRelativePath.test(referencedFile)
|
this.shouldLoadRelative = (referencedFile: string) => rRelativePath.test(referencedFile)
|
||||||
} else {
|
} else {
|
||||||
this.shouldLoadRelative = (referencedFile: string) => false
|
this.shouldLoadRelative = (_referencedFile: string) => false
|
||||||
}
|
}
|
||||||
this.contains = this.options.fs.contains || (() => true)
|
this.contains = this.options.fs.contains || (() => true)
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-19
@@ -2,25 +2,38 @@ import { MapFS } from './map-fs'
|
|||||||
|
|
||||||
describe('MapFS', () => {
|
describe('MapFS', () => {
|
||||||
const fs = new MapFS({})
|
const fs = new MapFS({})
|
||||||
it('should resolve relative file paths', () => {
|
describe('#resolve()', () => {
|
||||||
expect(fs.resolve('foo/bar', 'coo', '')).toEqual('foo/bar/coo')
|
it('should resolve relative file paths', () => {
|
||||||
|
expect(fs.resolve('foo/bar', 'coo', '')).toEqual('foo/bar/coo')
|
||||||
|
})
|
||||||
|
it('should resolve to parent', () => {
|
||||||
|
expect(fs.resolve('foo/bar', '../coo', '')).toEqual('foo/coo')
|
||||||
|
})
|
||||||
|
it('should resolve to root', () => {
|
||||||
|
expect(fs.resolve('foo/bar', '../../coo', '')).toEqual('coo')
|
||||||
|
})
|
||||||
|
it('should resolve exceeding root', () => {
|
||||||
|
expect(fs.resolve('foo/bar', '../../../coo', '')).toEqual('coo')
|
||||||
|
})
|
||||||
|
it('should resolve from absolute path', () => {
|
||||||
|
expect(fs.resolve('/foo/bar', '../../coo', '')).toEqual('/coo')
|
||||||
|
})
|
||||||
|
it('should resolve exceeding root from absolute path', () => {
|
||||||
|
expect(fs.resolve('/foo/bar', '../../../coo', '')).toEqual('/coo')
|
||||||
|
})
|
||||||
|
it('should resolve from invalid path', () => {
|
||||||
|
expect(fs.resolve('foo//bar', '../coo', '')).toEqual('foo/coo')
|
||||||
|
})
|
||||||
|
it('should resolve current path', () => {
|
||||||
|
expect(fs.resolve('foo/bar', '.././coo', '')).toEqual('foo/coo')
|
||||||
|
})
|
||||||
|
it('should resolve invalid path', () => {
|
||||||
|
expect(fs.resolve('foo/bar', '..//coo', '')).toEqual('foo/coo')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
it('should resolve to parent', () => {
|
describe('#.readFileSync()', () => {
|
||||||
expect(fs.resolve('foo/bar', '../coo', '')).toEqual('foo/coo')
|
it('should throw if not exist', () => {
|
||||||
})
|
expect(() => fs.readFileSync('foo/bar')).toThrow('NOENT: foo/bar')
|
||||||
it('should resolve to root', () => {
|
})
|
||||||
expect(fs.resolve('foo/bar', '../../coo', '')).toEqual('coo')
|
|
||||||
})
|
|
||||||
it('should resolve exceeding root', () => {
|
|
||||||
expect(fs.resolve('foo/bar', '../../../coo', '')).toEqual('coo')
|
|
||||||
})
|
|
||||||
it('should resolve from absolute path', () => {
|
|
||||||
expect(fs.resolve('/foo/bar', '../../coo', '')).toEqual('/coo')
|
|
||||||
})
|
|
||||||
it('should resolve exceeding root from absolute path', () => {
|
|
||||||
expect(fs.resolve('/foo/bar', '../../../coo', '')).toEqual('/coo')
|
|
||||||
})
|
|
||||||
it('should resolve from invalid path', () => {
|
|
||||||
expect(fs.resolve('foo//bar', '../coo', '')).toEqual('foo/coo')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/* istanbul ignore file */
|
/* istanbul ignore file */
|
||||||
export const version = '[VI]{version}[/VI]'
|
export const version = '[VI]{version}[/VI]'
|
||||||
export * as TypeGuards from './util/type-guards'
|
export * as TypeGuards from './util/type-guards'
|
||||||
export { toValue, TimezoneDate, createTrie, Trie, toPromise, toValueSync, assert, LiquidError, ParseError, RenderError, UndefinedVariableError, TokenizationError, AssertionError } from './util'
|
export { toValue, createTrie, Trie, toPromise, toValueSync, assert, LiquidError, ParseError, RenderError, UndefinedVariableError, TokenizationError, AssertionError } from './util'
|
||||||
export { Drop } from './drop'
|
export { Drop } from './drop'
|
||||||
export { Emitter } from './emitters'
|
export { Emitter } from './emitters'
|
||||||
export { defaultOperators, Operators, evalToken, evalQuotedToken, Expression, isFalsy, isTruthy } from './render'
|
export { defaultOperators, Operators, evalToken, evalQuotedToken, Expression, isFalsy, isTruthy } from './render'
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { assert, isArray, isString, isFunction } from './util'
|
import { assert, isArray, isString, isFunction } from './util'
|
||||||
|
import { getDateTimeFormat } from './util/intl'
|
||||||
import { LRU, LiquidCache } from './cache'
|
import { LRU, LiquidCache } from './cache'
|
||||||
import { FS, LookupType } from './fs'
|
import { FS, LookupType } from './fs'
|
||||||
import * as fs from './fs/fs-impl'
|
import * as fs from './fs/fs-impl'
|
||||||
@@ -43,6 +44,8 @@ export interface LiquidOptions {
|
|||||||
timezoneOffset?: number | string;
|
timezoneOffset?: number | string;
|
||||||
/** Default date format to use if the date filter doesn't include a format. Defaults to `%A, %B %-e, %Y at %-l:%M %P %z`. */
|
/** Default date format to use if the date filter doesn't include a format. Defaults to `%A, %B %-e, %Y at %-l:%M %P %z`. */
|
||||||
dateFormat?: string;
|
dateFormat?: string;
|
||||||
|
/** Default locale, will be used by date filter. Defaults to system locale. */
|
||||||
|
locale?: string;
|
||||||
/** Strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
|
/** Strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
|
||||||
trimTagRight?: boolean;
|
trimTagRight?: boolean;
|
||||||
/** Similar to `trimTagRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
|
/** Similar to `trimTagRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
|
||||||
@@ -138,6 +141,7 @@ export interface NormalizedFullOptions extends NormalizedOptions {
|
|||||||
ownPropertyOnly: boolean;
|
ownPropertyOnly: boolean;
|
||||||
lenientIf: boolean;
|
lenientIf: boolean;
|
||||||
dateFormat: string;
|
dateFormat: string;
|
||||||
|
locale: string;
|
||||||
trimTagRight: boolean;
|
trimTagRight: boolean;
|
||||||
trimTagLeft: boolean;
|
trimTagLeft: boolean;
|
||||||
trimOutputRight: boolean;
|
trimOutputRight: boolean;
|
||||||
@@ -168,6 +172,7 @@ export const defaultOptions: NormalizedFullOptions = {
|
|||||||
dynamicPartials: true,
|
dynamicPartials: true,
|
||||||
jsTruthy: false,
|
jsTruthy: false,
|
||||||
dateFormat: '%A, %B %-e, %Y at %-l:%M %P %z',
|
dateFormat: '%A, %B %-e, %Y at %-l:%M %P %z',
|
||||||
|
locale: '',
|
||||||
trimTagRight: false,
|
trimTagRight: false,
|
||||||
trimTagLeft: false,
|
trimTagLeft: false,
|
||||||
trimOutputRight: false,
|
trimOutputRight: false,
|
||||||
@@ -211,9 +216,9 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions {
|
|||||||
options.partials = normalizeDirectoryList(options.partials)
|
options.partials = normalizeDirectoryList(options.partials)
|
||||||
options.layouts = normalizeDirectoryList(options.layouts)
|
options.layouts = normalizeDirectoryList(options.layouts)
|
||||||
options.outputEscape = options.outputEscape && getOutputEscapeFunction(options.outputEscape)
|
options.outputEscape = options.outputEscape && getOutputEscapeFunction(options.outputEscape)
|
||||||
options.parseLimit = options.parseLimit || Infinity
|
if (!options.locale) {
|
||||||
options.renderLimit = options.renderLimit || Infinity
|
options.locale = getDateTimeFormat()?.().resolvedOptions().locale ?? 'en-US'
|
||||||
options.memoryLimit = options.memoryLimit || Infinity
|
}
|
||||||
if (options.templates) {
|
if (options.templates) {
|
||||||
options.fs = new MapFS(options.templates)
|
options.fs = new MapFS(options.templates)
|
||||||
options.relativeReference = true
|
options.relativeReference = true
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export class Tokenizer {
|
|||||||
return new HTMLToken(this.input, begin, this.p, this.file)
|
return new HTMLToken(this.input, begin, this.p, this.file)
|
||||||
}
|
}
|
||||||
|
|
||||||
readTagToken (options: NormalizedFullOptions = defaultOptions): TagToken {
|
readTagToken (options: NormalizedFullOptions): TagToken {
|
||||||
const { file, input } = this
|
const { file, input } = this
|
||||||
const begin = this.p
|
const begin = this.p
|
||||||
if (this.readToDelimiter(options.tagDelimiterRight) === -1) {
|
if (this.readToDelimiter(options.tagDelimiterRight) === -1) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Token } from './token'
|
import { Token } from './token'
|
||||||
import { NUMBER, TYPES, SIGN } from '../util'
|
|
||||||
import { TokenKind } from '../parser'
|
import { TokenKind } from '../parser'
|
||||||
|
|
||||||
export class IdentifierToken extends Token {
|
export class IdentifierToken extends Token {
|
||||||
@@ -13,13 +12,4 @@ export class IdentifierToken extends Token {
|
|||||||
super(TokenKind.Word, input, begin, end, file)
|
super(TokenKind.Word, input, begin, end, file)
|
||||||
this.content = this.getText()
|
this.content = this.getText()
|
||||||
}
|
}
|
||||||
isNumber (allowSign = false) {
|
|
||||||
const begin = allowSign && TYPES[this.input.charCodeAt(this.begin)] & SIGN
|
|
||||||
? this.begin + 1
|
|
||||||
: this.begin
|
|
||||||
for (let i = begin; i < this.end; i++) {
|
|
||||||
if (!(TYPES[this.input.charCodeAt(i)] & NUMBER)) return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Template } from '../template'
|
||||||
|
import { NumberToken } from '../tokens'
|
||||||
|
import { LiquidErrors, LiquidError, ParseError, RenderError } from './error'
|
||||||
|
|
||||||
|
describe('LiquidError', () => {
|
||||||
|
describe('.is()', () => {
|
||||||
|
it('should return true for a LiquidError instance', () => {
|
||||||
|
const err = new Error('intended')
|
||||||
|
const token = new NumberToken('3', 0, 1)
|
||||||
|
expect(LiquidError.is(new ParseError(err, token))).toBeTruthy()
|
||||||
|
})
|
||||||
|
it('should return false for null', () => {
|
||||||
|
expect(LiquidError.is(null)).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('LiquidErrors', () => {
|
||||||
|
describe('.is()', () => {
|
||||||
|
it('should return true for a LiquidErrors instance', () => {
|
||||||
|
const err = new Error('intended')
|
||||||
|
const token = new NumberToken('3', 0, 1)
|
||||||
|
const error = new ParseError(err, token)
|
||||||
|
expect(LiquidErrors.is(new LiquidErrors([error]))).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('RenderError', () => {
|
||||||
|
describe('.is()', () => {
|
||||||
|
it('should return true for a RenderError instance', () => {
|
||||||
|
const err = new Error('intended')
|
||||||
|
const tpl = {
|
||||||
|
token: new NumberToken('3', 0, 1),
|
||||||
|
render: () => ''
|
||||||
|
} as any as Template
|
||||||
|
expect(RenderError.is(new RenderError(err, tpl))).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
+1
-1
@@ -8,5 +8,5 @@ export * from './type-guards'
|
|||||||
export * from './async'
|
export * from './async'
|
||||||
export * from './strftime'
|
export * from './strftime'
|
||||||
export * from './liquid-date'
|
export * from './liquid-date'
|
||||||
export * from './timezone-date'
|
|
||||||
export * from './limiter'
|
export * from './limiter'
|
||||||
|
export * from './intl'
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function getDateTimeFormat () {
|
||||||
|
return (typeof Intl !== 'undefined' ? Intl.DateTimeFormat : undefined)
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { LiquidDate } from './liquid-date'
|
||||||
|
import { disableIntl } from '../../test/stub/no-intl'
|
||||||
|
|
||||||
|
describe('LiquidDate', () => {
|
||||||
|
describe('timezone', () => {
|
||||||
|
it('should respect timezone set to 00:00', () => {
|
||||||
|
const date = new LiquidDate('2021-10-06T14:26:00.000+08:00', 'en-US', 0)
|
||||||
|
expect(date.getTimezoneOffset()).toBe(0)
|
||||||
|
expect(date.getHours()).toBe(6)
|
||||||
|
expect(date.getMinutes()).toBe(26)
|
||||||
|
})
|
||||||
|
it('should respect timezone set to -06:00', () => {
|
||||||
|
const date = new LiquidDate('2021-10-06T14:26:00.000+08:00', 'en-US', -360)
|
||||||
|
expect(date.getTimezoneOffset()).toBe(-360)
|
||||||
|
expect(date.getMinutes()).toBe(26)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('should support Date as argument', () => {
|
||||||
|
const date = new LiquidDate(new Date('2021-10-06T14:26:00.000+08:00'), 'en-US', 0)
|
||||||
|
expect(date.getHours()).toBe(6)
|
||||||
|
})
|
||||||
|
it('should support .getMilliseconds()', () => {
|
||||||
|
const date = new LiquidDate('2021-10-06T14:26:00.001+00:00', 'en-US', 0)
|
||||||
|
expect(date.getMilliseconds()).toBe(1)
|
||||||
|
})
|
||||||
|
it('should support .getDay()', () => {
|
||||||
|
const date = new LiquidDate('2021-12-07T00:00:00.001+08:00', 'en-US', -480)
|
||||||
|
expect(date.getDay()).toBe(2)
|
||||||
|
})
|
||||||
|
it('should support .toLocaleString()', () => {
|
||||||
|
const date = new LiquidDate('2021-10-06T00:00:00.001+00:00', 'en-US', -480)
|
||||||
|
expect(date.toLocaleString('en-US')).toMatch(/8:00:00\sAM$/)
|
||||||
|
expect(date.toLocaleString('en-US', { timeZone: 'America/New_York' })).toMatch(/8:00:00\sPM$/)
|
||||||
|
expect(() => date.toLocaleString()).not.toThrow()
|
||||||
|
})
|
||||||
|
it('should support .toLocaleTimeString()', () => {
|
||||||
|
const date = new LiquidDate('2021-10-06T00:00:00.001+00:00', 'en-US', -480)
|
||||||
|
expect(date.toLocaleTimeString('en-US')).toMatch(/^8:00:00\sAM$/)
|
||||||
|
expect(() => date.toLocaleDateString()).not.toThrow()
|
||||||
|
})
|
||||||
|
it('should support .toLocaleDateString()', () => {
|
||||||
|
const date = new LiquidDate('2021-10-06T22:00:00.001+00:00', 'en-US', -480)
|
||||||
|
expect(date.toLocaleDateString('en-US')).toBe('10/7/2021')
|
||||||
|
expect(() => date.toLocaleDateString()).not.toThrow()
|
||||||
|
})
|
||||||
|
describe('compatibility', () => {
|
||||||
|
disableIntl()
|
||||||
|
it('should use English months if Intl.DateTimeFormat not supported', () => {
|
||||||
|
expect(new LiquidDate('2021-10-06T22:00:00.001+00:00', 'en-US', -480).getLongMonthName()).toEqual('October')
|
||||||
|
expect(new LiquidDate('2021-10-06T22:00:00.001+00:00', 'zh-CN', -480).getLongMonthName()).toEqual('October')
|
||||||
|
expect(new LiquidDate('2021-10-06T22:00:00.001+00:00', 'zh-CN', -480).getShortMonthName()).toEqual('Oct')
|
||||||
|
})
|
||||||
|
it('should use English weekdays if Intl.DateTimeFormat not supported', () => {
|
||||||
|
expect(new LiquidDate('2024-07-21T22:00:00.001+00:00', 'en-US', 0).getLongWeekdayName()).toEqual('Sunday')
|
||||||
|
expect(new LiquidDate('2024-07-21T22:00:00.001+00:00', 'zh-CN', -480).getLongWeekdayName()).toEqual('Monday')
|
||||||
|
expect(new LiquidDate('2024-07-21T22:00:00.001+00:00', 'zh-CN', -480).getShortWeekdayName()).toEqual('Mon')
|
||||||
|
})
|
||||||
|
it('should return none for timezone if Intl.DateTimeFormat not supported', () => {
|
||||||
|
expect(new LiquidDate('2024-07-21T22:00:00.001', 'en-US').getTimeZoneName()).toEqual(undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
+147
-17
@@ -1,20 +1,150 @@
|
|||||||
|
import { getDateTimeFormat } from './intl'
|
||||||
|
import { isString } from './underscore'
|
||||||
|
|
||||||
|
// one minute in milliseconds
|
||||||
|
const OneMinute = 60000
|
||||||
|
const ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/
|
||||||
|
const monthNames = [
|
||||||
|
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
|
||||||
|
'September', 'October', 'November', 'December'
|
||||||
|
]
|
||||||
|
const monthNamesShort = monthNames.map(name => name.slice(0, 3))
|
||||||
|
const dayNames = [
|
||||||
|
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
|
||||||
|
]
|
||||||
|
const dayNamesShort = dayNames.map(name => name.slice(0, 3))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date interface LiquidJS uses.
|
* A date implementation with timezone info, just like Ruby date
|
||||||
* Basically a subset of JavaScript Date,
|
*
|
||||||
* it's defined abstractly here to allow different implementation
|
* Implementation:
|
||||||
|
* - create a Date offset by it's timezone difference, avoiding overriding a bunch of methods
|
||||||
|
* - rewrite getTimezoneOffset() to trick strftime
|
||||||
*/
|
*/
|
||||||
export interface LiquidDate {
|
export class LiquidDate {
|
||||||
getTime(): number;
|
private timezoneOffset: number
|
||||||
getMilliseconds(): number;
|
private timezoneName: string
|
||||||
getSeconds(): number;
|
private date: Date
|
||||||
getMinutes(): number;
|
private displayDate: Date
|
||||||
getHours(): number;
|
private DateTimeFormat = getDateTimeFormat()
|
||||||
getDay(): number;
|
public timezoneFixed: boolean
|
||||||
getDate(): number;
|
constructor (
|
||||||
getMonth(): number;
|
init: string | number | Date,
|
||||||
getFullYear(): number;
|
private locale: string,
|
||||||
getTimezoneOffset(): number;
|
timezone?: number | string
|
||||||
getTimezoneName?(): string;
|
) {
|
||||||
toLocaleTimeString(): string;
|
this.date = new Date(init)
|
||||||
toLocaleDateString(): string;
|
this.timezoneFixed = timezone !== undefined
|
||||||
|
if (timezone === undefined) {
|
||||||
|
timezone = this.date.getTimezoneOffset()
|
||||||
|
}
|
||||||
|
this.timezoneOffset = isString(timezone) ? LiquidDate.getTimezoneOffset(timezone, this.date) : timezone
|
||||||
|
this.timezoneName = isString(timezone) ? timezone : ''
|
||||||
|
|
||||||
|
const diff = (this.date.getTimezoneOffset() - this.timezoneOffset) * OneMinute
|
||||||
|
const time = this.date.getTime() + diff
|
||||||
|
this.displayDate = new Date(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
getTime () {
|
||||||
|
return this.displayDate.getTime()
|
||||||
|
}
|
||||||
|
getMilliseconds () {
|
||||||
|
return this.displayDate.getMilliseconds()
|
||||||
|
}
|
||||||
|
getSeconds () {
|
||||||
|
return this.displayDate.getSeconds()
|
||||||
|
}
|
||||||
|
getMinutes () {
|
||||||
|
return this.displayDate.getMinutes()
|
||||||
|
}
|
||||||
|
getHours () {
|
||||||
|
return this.displayDate.getHours()
|
||||||
|
}
|
||||||
|
getDay () {
|
||||||
|
return this.displayDate.getDay()
|
||||||
|
}
|
||||||
|
getDate () {
|
||||||
|
return this.displayDate.getDate()
|
||||||
|
}
|
||||||
|
getMonth () {
|
||||||
|
return this.displayDate.getMonth()
|
||||||
|
}
|
||||||
|
getFullYear () {
|
||||||
|
return this.displayDate.getFullYear()
|
||||||
|
}
|
||||||
|
toLocaleString (locale?: string, init?: any) {
|
||||||
|
if (init?.timeZone) {
|
||||||
|
return this.date.toLocaleString(locale, init)
|
||||||
|
}
|
||||||
|
return this.displayDate.toLocaleString(locale, init)
|
||||||
|
}
|
||||||
|
toLocaleTimeString (locale?: string) {
|
||||||
|
return this.displayDate.toLocaleTimeString(locale)
|
||||||
|
}
|
||||||
|
toLocaleDateString (locale?: string) {
|
||||||
|
return this.displayDate.toLocaleDateString(locale)
|
||||||
|
}
|
||||||
|
getTimezoneOffset () {
|
||||||
|
return this.timezoneOffset!
|
||||||
|
}
|
||||||
|
getTimeZoneName () {
|
||||||
|
if (this.timezoneFixed) return this.timezoneName
|
||||||
|
if (!this.DateTimeFormat) return
|
||||||
|
return this.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
}
|
||||||
|
getLongMonthName () {
|
||||||
|
return this.format({ month: 'long' }) ?? monthNames[this.getMonth()]
|
||||||
|
}
|
||||||
|
getShortMonthName () {
|
||||||
|
return this.format({ month: 'short' }) ?? monthNamesShort[this.getMonth()]
|
||||||
|
}
|
||||||
|
getLongWeekdayName () {
|
||||||
|
return this.format({ weekday: 'long' }) ?? dayNames[this.displayDate.getDay()]
|
||||||
|
}
|
||||||
|
getShortWeekdayName () {
|
||||||
|
return this.format({ weekday: 'short' }) ?? dayNamesShort[this.displayDate.getDay()]
|
||||||
|
}
|
||||||
|
valid () {
|
||||||
|
return !isNaN(this.getTime())
|
||||||
|
}
|
||||||
|
private format (options: Intl.DateTimeFormatOptions) {
|
||||||
|
return this.DateTimeFormat && this.DateTimeFormat(this.locale, options).format(this.displayDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Date object fixed to it's declared Timezone. Both
|
||||||
|
* - 2021-08-06T02:29:00.000Z and
|
||||||
|
* - 2021-08-06T02:29:00.000+08:00
|
||||||
|
* will always be displayed as
|
||||||
|
* - 2021-08-06 02:29:00
|
||||||
|
* regardless timezoneOffset in JavaScript realm
|
||||||
|
*
|
||||||
|
* The implementation hack:
|
||||||
|
* Instead of calling `.getMonth()`/`.getUTCMonth()` respect to `preserveTimezones`,
|
||||||
|
* we create a different Date to trick strftime, it's both simpler and more performant.
|
||||||
|
* Given that a template is expected to be parsed fewer times than rendered.
|
||||||
|
*/
|
||||||
|
static createDateFixedToTimezone (dateString: string, locale: string): LiquidDate {
|
||||||
|
const m = dateString.match(ISO8601_TIMEZONE_PATTERN)
|
||||||
|
// representing a UTC timestamp
|
||||||
|
if (m && m[1] === 'Z') {
|
||||||
|
return new LiquidDate(+new Date(dateString), locale, 0)
|
||||||
|
}
|
||||||
|
// has a timezone specified
|
||||||
|
if (m && m[2] && m[3] && m[4]) {
|
||||||
|
const [, , sign, hours, minutes] = m
|
||||||
|
const offset = (sign === '+' ? -1 : 1) * (parseInt(hours, 10) * 60 + parseInt(minutes, 10))
|
||||||
|
return new LiquidDate(+new Date(dateString), locale, offset)
|
||||||
|
}
|
||||||
|
return new LiquidDate(dateString, locale)
|
||||||
|
}
|
||||||
|
private static getTimezoneOffset (timezoneName: string, date: Date) {
|
||||||
|
const localDateString = date.toLocaleString('en-US', { timeZone: timezoneName })
|
||||||
|
const utcDateString = date.toLocaleString('en-US', { timeZone: 'UTC' })
|
||||||
|
|
||||||
|
const localDate = new Date(localDateString)
|
||||||
|
const utcDate = new Date(utcDateString)
|
||||||
|
return (+utcDate - +localDate) / (60 * 1000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-17
@@ -1,9 +1,9 @@
|
|||||||
import { strftime as t } from './strftime'
|
import { strftime as t } from './strftime'
|
||||||
import { DateWithTimezone } from '../../test/stub/date-with-timezone'
|
import { DateWithTimezone, TestDate } from '../../test/stub/date'
|
||||||
|
|
||||||
describe('util/strftime', function () {
|
describe('util/strftime', function () {
|
||||||
const now = new Date('2016-01-04 13:15:23')
|
const now = new TestDate('2016-01-04 13:15:23')
|
||||||
const then = new Date('2016-03-06 03:05:03')
|
const then = new TestDate('2016-03-06 03:05:03')
|
||||||
|
|
||||||
describe('Date (Year, Month, Day)', () => {
|
describe('Date (Year, Month, Day)', () => {
|
||||||
it('should format %C as century', function () {
|
it('should format %C as century', function () {
|
||||||
@@ -23,26 +23,26 @@ describe('util/strftime', function () {
|
|||||||
expect(t(then, '%j')).toBe('066')
|
expect(t(then, '%j')).toBe('066')
|
||||||
})
|
})
|
||||||
it('should take count of leap years', function () {
|
it('should take count of leap years', function () {
|
||||||
const date = new Date('2001 03 01')
|
const date = new TestDate('2001 03 01')
|
||||||
expect(t(date, '%j')).toBe('060')
|
expect(t(date, '%j')).toBe('060')
|
||||||
})
|
})
|
||||||
it('should take count of leap years', function () {
|
it('should take count of leap years', function () {
|
||||||
const date = new Date('2000 03 01')
|
const date = new TestDate('2000 03 01')
|
||||||
expect(t(date, '%j')).toBe('061')
|
expect(t(date, '%j')).toBe('061')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
it('should format %q as date suffix', function () {
|
it('should format %q as date suffix', function () {
|
||||||
const first = new Date('2016-03-01 03:05:03')
|
const first = new TestDate('2016-03-01 03:05:03')
|
||||||
const second = new Date('2016-03-02 03:05:03')
|
const second = new TestDate('2016-03-02 03:05:03')
|
||||||
const third = new Date('2016-03-03 03:05:03')
|
const third = new TestDate('2016-03-03 03:05:03')
|
||||||
|
|
||||||
const eleventh = new Date('2016-03-11 03:05:03')
|
const eleventh = new TestDate('2016-03-11 03:05:03')
|
||||||
const twelfth = new Date('2016-03-12 03:05:03')
|
const twelfth = new TestDate('2016-03-12 03:05:03')
|
||||||
const thirteenth = new Date('2016-03-13 03:05:03')
|
const thirteenth = new TestDate('2016-03-13 03:05:03')
|
||||||
|
|
||||||
const twentyfirst = new Date('2016-03-21 03:05:03')
|
const twentyfirst = new TestDate('2016-03-21 03:05:03')
|
||||||
const twentysecond = new Date('2016-03-22 03:05:03')
|
const twentysecond = new TestDate('2016-03-22 03:05:03')
|
||||||
const twentythird = new Date('2016-03-23 03:05:03')
|
const twentythird = new TestDate('2016-03-23 03:05:03')
|
||||||
|
|
||||||
expect(t(first, '%q')).toBe('st')
|
expect(t(first, '%q')).toBe('st')
|
||||||
expect(t(second, '%q')).toBe('nd')
|
expect(t(second, '%q')).toBe('nd')
|
||||||
@@ -64,7 +64,7 @@ describe('util/strftime', function () {
|
|||||||
expect(t(now, '%I')).toBe('01')
|
expect(t(now, '%I')).toBe('01')
|
||||||
})
|
})
|
||||||
it('should format %I as 12 for 00:00', function () {
|
it('should format %I as 12 for 00:00', function () {
|
||||||
const date = new Date('2016-01-01 00:00:00')
|
const date = new TestDate('2016-01-01 00:00:00')
|
||||||
expect(t(date, '%I')).toBe('12')
|
expect(t(date, '%I')).toBe('12')
|
||||||
})
|
})
|
||||||
it('should format %k as space padded hour', function () {
|
it('should format %k as space padded hour', function () {
|
||||||
@@ -74,14 +74,14 @@ describe('util/strftime', function () {
|
|||||||
expect(t(now, '%l')).toBe(' 1')
|
expect(t(now, '%l')).toBe(' 1')
|
||||||
})
|
})
|
||||||
it('should format %l as 12 for 00:00', function () {
|
it('should format %l as 12 for 00:00', function () {
|
||||||
const date = new Date('2016-01-01 00:00:00')
|
const date = new TestDate('2016-01-01 00:00:00')
|
||||||
expect(t(date, '%l')).toBe('12')
|
expect(t(date, '%l')).toBe('12')
|
||||||
})
|
})
|
||||||
it('should format %L as 0 padded millisecond', function () {
|
it('should format %L as 0 padded millisecond', function () {
|
||||||
expect(t(then, '%L')).toBe('000')
|
expect(t(then, '%L')).toBe('000')
|
||||||
})
|
})
|
||||||
it('should format %N as fractional seconds digits', function () {
|
it('should format %N as fractional seconds digits', function () {
|
||||||
const time = new Date('2019-12-15 01:21:00.129')
|
const time = new TestDate('2019-12-15 01:21:00.129')
|
||||||
expect(t(time, '%N')).toBe('129000000')
|
expect(t(time, '%N')).toBe('129000000')
|
||||||
expect(t(time, '%2N')).toBe('12')
|
expect(t(time, '%2N')).toBe('12')
|
||||||
expect(t(time, '%10N')).toBe('1290000000')
|
expect(t(time, '%10N')).toBe('1290000000')
|
||||||
|
|||||||
+8
-37
@@ -2,25 +2,12 @@ import { changeCase, padStart, padEnd } from './underscore'
|
|||||||
import { LiquidDate } from './liquid-date'
|
import { LiquidDate } from './liquid-date'
|
||||||
|
|
||||||
const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/
|
const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/
|
||||||
const monthNames = [
|
|
||||||
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
|
|
||||||
'September', 'October', 'November', 'December'
|
|
||||||
]
|
|
||||||
const dayNames = [
|
|
||||||
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
|
|
||||||
]
|
|
||||||
const monthNamesShort = monthNames.map(abbr)
|
|
||||||
const dayNamesShort = dayNames.map(abbr)
|
|
||||||
interface FormatOptions {
|
interface FormatOptions {
|
||||||
flags: object;
|
flags: object;
|
||||||
width?: string;
|
width?: string;
|
||||||
modifier?: string;
|
modifier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function abbr (str: string) {
|
|
||||||
return str.slice(0, 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// prototype extensions
|
// prototype extensions
|
||||||
function daysInMonth (d: LiquidDate) {
|
function daysInMonth (d: LiquidDate) {
|
||||||
const feb = isLeapYear(d) ? 29 : 28
|
const feb = isLeapYear(d) ? 29 : 28
|
||||||
@@ -77,19 +64,8 @@ const padWidths = {
|
|||||||
W: 2
|
W: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
// default to '0'
|
const padSpaceChars = new Set('aAbBceklpP')
|
||||||
const padChars = {
|
|
||||||
a: ' ',
|
|
||||||
A: ' ',
|
|
||||||
b: ' ',
|
|
||||||
B: ' ',
|
|
||||||
c: ' ',
|
|
||||||
e: ' ',
|
|
||||||
k: ' ',
|
|
||||||
l: ' ',
|
|
||||||
p: ' ',
|
|
||||||
P: ' '
|
|
||||||
}
|
|
||||||
function getTimezoneOffset (d: LiquidDate, opts: FormatOptions) {
|
function getTimezoneOffset (d: LiquidDate, opts: FormatOptions) {
|
||||||
const nOffset = Math.abs(d.getTimezoneOffset())
|
const nOffset = Math.abs(d.getTimezoneOffset())
|
||||||
const h = Math.floor(nOffset / 60)
|
const h = Math.floor(nOffset / 60)
|
||||||
@@ -100,10 +76,10 @@ function getTimezoneOffset (d: LiquidDate, opts: FormatOptions) {
|
|||||||
padStart(m, 2, '0')
|
padStart(m, 2, '0')
|
||||||
}
|
}
|
||||||
const formatCodes = {
|
const formatCodes = {
|
||||||
a: (d: LiquidDate) => dayNamesShort[d.getDay()],
|
a: (d: LiquidDate) => d.getShortWeekdayName(),
|
||||||
A: (d: LiquidDate) => dayNames[d.getDay()],
|
A: (d: LiquidDate) => d.getLongWeekdayName(),
|
||||||
b: (d: LiquidDate) => monthNamesShort[d.getMonth()],
|
b: (d: LiquidDate) => d.getShortMonthName(),
|
||||||
B: (d: LiquidDate) => monthNames[d.getMonth()],
|
B: (d: LiquidDate) => d.getLongMonthName(),
|
||||||
c: (d: LiquidDate) => d.toLocaleString(),
|
c: (d: LiquidDate) => d.toLocaleString(),
|
||||||
C: (d: LiquidDate) => century(d),
|
C: (d: LiquidDate) => century(d),
|
||||||
d: (d: LiquidDate) => d.getDate(),
|
d: (d: LiquidDate) => d.getDate(),
|
||||||
@@ -135,12 +111,7 @@ const formatCodes = {
|
|||||||
y: (d: LiquidDate) => d.getFullYear().toString().slice(2, 4),
|
y: (d: LiquidDate) => d.getFullYear().toString().slice(2, 4),
|
||||||
Y: (d: LiquidDate) => d.getFullYear(),
|
Y: (d: LiquidDate) => d.getFullYear(),
|
||||||
z: getTimezoneOffset,
|
z: getTimezoneOffset,
|
||||||
Z: (d: LiquidDate, opts: FormatOptions) => {
|
Z: (d: LiquidDate, opts: FormatOptions) => d.getTimeZoneName() || getTimezoneOffset(d, opts),
|
||||||
if (d.getTimezoneName) {
|
|
||||||
return d.getTimezoneName() || getTimezoneOffset(d, opts)
|
|
||||||
}
|
|
||||||
return (typeof Intl !== 'undefined' ? Intl.DateTimeFormat().resolvedOptions().timeZone : '')
|
|
||||||
},
|
|
||||||
't': () => '\t',
|
't': () => '\t',
|
||||||
'n': () => '\n',
|
'n': () => '\n',
|
||||||
'%': () => '%'
|
'%': () => '%'
|
||||||
@@ -166,7 +137,7 @@ function format (d: LiquidDate, match: RegExpExecArray) {
|
|||||||
const flags = {}
|
const flags = {}
|
||||||
for (const flag of flagStr) flags[flag] = true
|
for (const flag of flagStr) flags[flag] = true
|
||||||
let ret = String(convert(d, { flags, width, modifier }))
|
let ret = String(convert(d, { flags, width, modifier }))
|
||||||
let padChar = padChars[conversion] || '0'
|
let padChar = padSpaceChars.has(conversion) ? ' ' : '0'
|
||||||
let padWidth = width || padWidths[conversion] || 0
|
let padWidth = width || padWidths[conversion] || 0
|
||||||
if (flags['^']) ret = ret.toUpperCase()
|
if (flags['^']) ret = ret.toUpperCase()
|
||||||
else if (flags['#']) ret = changeCase(ret)
|
else if (flags['#']) ret = changeCase(ret)
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import { TimezoneDate } from './timezone-date'
|
|
||||||
|
|
||||||
describe('TimezoneDate', () => {
|
|
||||||
it('should respect timezone set to 00:00', () => {
|
|
||||||
const date = new TimezoneDate('2021-10-06T14:26:00.000+08:00', 0)
|
|
||||||
expect(date.getTimezoneOffset()).toBe(0)
|
|
||||||
expect(date.getHours()).toBe(6)
|
|
||||||
expect(date.getMinutes()).toBe(26)
|
|
||||||
})
|
|
||||||
it('should respect timezone set to -06:00', () => {
|
|
||||||
const date = new TimezoneDate('2021-10-06T14:26:00.000+08:00', -360)
|
|
||||||
expect(date.getTimezoneOffset()).toBe(-360)
|
|
||||||
expect(date.getMinutes()).toBe(26)
|
|
||||||
})
|
|
||||||
it('should support Date as argument', () => {
|
|
||||||
const date = new TimezoneDate(new Date('2021-10-06T14:26:00.000+08:00'), 0)
|
|
||||||
expect(date.getHours()).toBe(6)
|
|
||||||
})
|
|
||||||
it('should support .getMilliseconds()', () => {
|
|
||||||
const date = new TimezoneDate('2021-10-06T14:26:00.001+00:00', 0)
|
|
||||||
expect(date.getMilliseconds()).toBe(1)
|
|
||||||
})
|
|
||||||
it('should support .getDay()', () => {
|
|
||||||
const date = new TimezoneDate('2021-12-07T00:00:00.001+08:00', -480)
|
|
||||||
expect(date.getDay()).toBe(2)
|
|
||||||
})
|
|
||||||
it('should support .toLocaleString()', () => {
|
|
||||||
const date = new TimezoneDate('2021-10-06T00:00:00.001+00:00', -480)
|
|
||||||
expect(date.toLocaleString('en-US')).toMatch(/8:00:00\sAM$/)
|
|
||||||
expect(date.toLocaleString('en-US', { timeZone: 'America/New_York' })).toMatch(/8:00:00\sPM$/)
|
|
||||||
expect(() => date.toLocaleString()).not.toThrow()
|
|
||||||
})
|
|
||||||
it('should support .toLocaleTimeString()', () => {
|
|
||||||
const date = new TimezoneDate('2021-10-06T00:00:00.001+00:00', -480)
|
|
||||||
expect(date.toLocaleTimeString('en-US')).toMatch(/^8:00:00\sAM$/)
|
|
||||||
expect(() => date.toLocaleDateString()).not.toThrow()
|
|
||||||
})
|
|
||||||
it('should support .toLocaleDateString()', () => {
|
|
||||||
const date = new TimezoneDate('2021-10-06T22:00:00.001+00:00', -480)
|
|
||||||
expect(date.toLocaleDateString('en-US')).toBe('10/7/2021')
|
|
||||||
expect(() => date.toLocaleDateString()).not.toThrow()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
import { LiquidDate } from './liquid-date'
|
|
||||||
import { isString } from './underscore'
|
|
||||||
|
|
||||||
// one minute in milliseconds
|
|
||||||
const OneMinute = 60000
|
|
||||||
const ISO8601_TIMEZONE_PATTERN = /([zZ]|([+-])(\d{2}):(\d{2}))$/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A date implementation with timezone info, just like Ruby date
|
|
||||||
*
|
|
||||||
* Implementation:
|
|
||||||
* - create a Date offset by it's timezone difference, avoiding overriding a bunch of methods
|
|
||||||
* - rewrite getTimezoneOffset() to trick strftime
|
|
||||||
*/
|
|
||||||
export class TimezoneDate implements LiquidDate {
|
|
||||||
private timezoneOffset: number
|
|
||||||
private timezoneName: string
|
|
||||||
private date: Date
|
|
||||||
private displayDate: Date
|
|
||||||
constructor (init: string | number | Date | TimezoneDate, timezone: number | string) {
|
|
||||||
this.date = init instanceof TimezoneDate
|
|
||||||
? init.date
|
|
||||||
: new Date(init)
|
|
||||||
this.timezoneOffset = isString(timezone) ? TimezoneDate.getTimezoneOffset(timezone, this.date) : timezone
|
|
||||||
this.timezoneName = isString(timezone) ? timezone : ''
|
|
||||||
|
|
||||||
const diff = (this.date.getTimezoneOffset() - this.timezoneOffset) * OneMinute
|
|
||||||
const time = this.date.getTime() + diff
|
|
||||||
this.displayDate = new Date(time)
|
|
||||||
}
|
|
||||||
|
|
||||||
getTime () {
|
|
||||||
return this.displayDate.getTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
getMilliseconds () {
|
|
||||||
return this.displayDate.getMilliseconds()
|
|
||||||
}
|
|
||||||
getSeconds () {
|
|
||||||
return this.displayDate.getSeconds()
|
|
||||||
}
|
|
||||||
getMinutes () {
|
|
||||||
return this.displayDate.getMinutes()
|
|
||||||
}
|
|
||||||
getHours () {
|
|
||||||
return this.displayDate.getHours()
|
|
||||||
}
|
|
||||||
getDay () {
|
|
||||||
return this.displayDate.getDay()
|
|
||||||
}
|
|
||||||
getDate () {
|
|
||||||
return this.displayDate.getDate()
|
|
||||||
}
|
|
||||||
getMonth () {
|
|
||||||
return this.displayDate.getMonth()
|
|
||||||
}
|
|
||||||
getFullYear () {
|
|
||||||
return this.displayDate.getFullYear()
|
|
||||||
}
|
|
||||||
toLocaleString (locale?: string, init?: any) {
|
|
||||||
if (init?.timeZone) {
|
|
||||||
return this.date.toLocaleString(locale, init)
|
|
||||||
}
|
|
||||||
return this.displayDate.toLocaleString(locale, init)
|
|
||||||
}
|
|
||||||
toLocaleTimeString (locale?: string) {
|
|
||||||
return this.displayDate.toLocaleTimeString(locale)
|
|
||||||
}
|
|
||||||
toLocaleDateString (locale?: string) {
|
|
||||||
return this.displayDate.toLocaleDateString(locale)
|
|
||||||
}
|
|
||||||
getTimezoneOffset () {
|
|
||||||
return this.timezoneOffset!
|
|
||||||
}
|
|
||||||
getTimezoneName () {
|
|
||||||
return this.timezoneName
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a Date object fixed to it's declared Timezone. Both
|
|
||||||
* - 2021-08-06T02:29:00.000Z and
|
|
||||||
* - 2021-08-06T02:29:00.000+08:00
|
|
||||||
* will always be displayed as
|
|
||||||
* - 2021-08-06 02:29:00
|
|
||||||
* regardless timezoneOffset in JavaScript realm
|
|
||||||
*
|
|
||||||
* The implementation hack:
|
|
||||||
* Instead of calling `.getMonth()`/`.getUTCMonth()` respect to `preserveTimezones`,
|
|
||||||
* we create a different Date to trick strftime, it's both simpler and more performant.
|
|
||||||
* Given that a template is expected to be parsed fewer times than rendered.
|
|
||||||
*/
|
|
||||||
static createDateFixedToTimezone (dateString: string): LiquidDate {
|
|
||||||
const m = dateString.match(ISO8601_TIMEZONE_PATTERN)
|
|
||||||
// representing a UTC timestamp
|
|
||||||
if (m && m[1] === 'Z') {
|
|
||||||
return new TimezoneDate(+new Date(dateString), 0)
|
|
||||||
}
|
|
||||||
// has a timezone specified
|
|
||||||
if (m && m[2] && m[3] && m[4]) {
|
|
||||||
const [, , sign, hours, minutes] = m
|
|
||||||
const offset = (sign === '+' ? -1 : 1) * (parseInt(hours, 10) * 60 + parseInt(minutes, 10))
|
|
||||||
return new TimezoneDate(+new Date(dateString), offset)
|
|
||||||
}
|
|
||||||
return new Date(dateString)
|
|
||||||
}
|
|
||||||
private static getTimezoneOffset (timezoneName: string, date = new Date()) {
|
|
||||||
const localDateString = date.toLocaleString('en-US', { timeZone: timezoneName })
|
|
||||||
const utcDateString = date.toLocaleString('en-US', { timeZone: 'UTC' })
|
|
||||||
|
|
||||||
const localDate = new Date(localDateString)
|
|
||||||
const utcDate = new Date(utcDateString)
|
|
||||||
return (+utcDate - +localDate) / (60 * 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { LiteralToken } from '../tokens'
|
||||||
|
import { isLiteralToken, isNumberToken, isWordToken } from './type-guards'
|
||||||
|
|
||||||
|
describe('isLiteralToken()', () => {
|
||||||
|
it('should return true for literal', () => {
|
||||||
|
expect(isLiteralToken(new LiteralToken('true', 0, 4))).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('isWordToken()', () => {
|
||||||
|
it('should return false for literal', () => {
|
||||||
|
expect(isWordToken(new LiteralToken('true', 0, 4))).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('isNumberToken()', () => {
|
||||||
|
it('should return false for literal', () => {
|
||||||
|
expect(isNumberToken(new LiteralToken('true', 0, 4))).toBeFalsy()
|
||||||
|
})
|
||||||
|
it('should return false for null', () => {
|
||||||
|
expect(isNumberToken(null)).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -475,6 +475,15 @@ describe('filters/array', function () {
|
|||||||
- 2
|
- 2
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
|
it('should render none if args not specified', function () {
|
||||||
|
return test(`{% assign kitchen_products = products | where %}
|
||||||
|
Kitchen products:
|
||||||
|
{% for product in kitchen_products -%}
|
||||||
|
- {{ product.title }}
|
||||||
|
{% endfor %}`, { products }, `
|
||||||
|
Kitchen products:
|
||||||
|
`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
describe('where_exp', function () {
|
describe('where_exp', function () {
|
||||||
const products = [
|
const products = [
|
||||||
@@ -560,6 +569,12 @@ describe('filters/array', function () {
|
|||||||
{ members },
|
{ members },
|
||||||
`{"graduation_year":2014,"name":"John"}`)
|
`{"graduation_year":2014,"name":"John"}`)
|
||||||
})
|
})
|
||||||
|
it('should render none if not found', function () {
|
||||||
|
return test(
|
||||||
|
`{{ members | find: "graduation_year", 2018 | json }}`,
|
||||||
|
{ members },
|
||||||
|
``)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
describe('find_exp', function () {
|
describe('find_exp', function () {
|
||||||
const members = [
|
const members = [
|
||||||
@@ -573,5 +588,11 @@ describe('filters/array', function () {
|
|||||||
{ members },
|
{ members },
|
||||||
`{"graduation_year":2014,"name":"John"}`)
|
`{"graduation_year":2014,"name":"John"}`)
|
||||||
})
|
})
|
||||||
|
it('should render none if not found', function () {
|
||||||
|
return test(
|
||||||
|
`{{ members | find_exp: "item", "item.graduation_year == 2018" | json }}`,
|
||||||
|
{ members },
|
||||||
|
``)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,22 +1,81 @@
|
|||||||
import { LiquidOptions } from '../../../src/liquid-options'
|
import { LiquidOptions } from '../../../src/liquid-options'
|
||||||
import { Liquid } from '.././../../src/liquid'
|
import { Liquid } from '.././../../src/liquid'
|
||||||
import { test } from '../../stub/render'
|
import { test } from '../../stub/render'
|
||||||
|
import { disableIntl } from '../../stub/no-intl'
|
||||||
|
|
||||||
describe('filters/date', function () {
|
describe('filters/date', function () {
|
||||||
|
const liquid = new Liquid({ locale: 'en-US' })
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should create a new Date when given "now"', function () {
|
||||||
|
return test('{{ "now" | date: "%Y"}}', (new Date()).getFullYear().toString())
|
||||||
|
})
|
||||||
|
it('should create a new Date when given "today"', function () {
|
||||||
|
return test('{{ "today" | date: "%Y"}}', (new Date()).getFullYear().toString())
|
||||||
|
})
|
||||||
|
it('should create from number', async function () {
|
||||||
|
const time = new Date('2017-03-07T12:00:00').getTime() / 1000
|
||||||
|
return test('{{ time | date: "%Y-%m-%dT%H:%M:%S" }}', { time }, '2017-03-07T12:00:00')
|
||||||
|
})
|
||||||
|
it('should create from number-like string', async function () {
|
||||||
|
const time = String(new Date('2017-03-07T12:00:00').getTime() / 1000)
|
||||||
|
return test('{{ time | date: "%Y-%m-%dT%H:%M:%S" }}', { time }, '2017-03-07T12:00:00')
|
||||||
|
})
|
||||||
|
it('should treat nil as 0', () => {
|
||||||
|
expect(liquid.parseAndRenderSync('{{ nil | date: "%Y-%m-%dT%H:%M:%S", "Asia/Shanghai" }}')).toEqual('1970-01-01T08:00:00')
|
||||||
|
})
|
||||||
|
it('should treat undefined as invalid', () => {
|
||||||
|
expect(liquid.parseAndRenderSync('{{ num | date: "%Y-%m-%dT%H:%M:%S", "Asia/Shanghai" }}', { num: undefined })).toEqual('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('should support date: %a %b %d %Y', function () {
|
it('should support date: %a %b %d %Y', function () {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
return test('{{ date | date:"%a %b %d %Y"}}', { date }, date.toDateString())
|
return test('{{ date | date:"%a %b %d %Y"}}', { date }, date.toDateString())
|
||||||
})
|
})
|
||||||
|
describe('%a', () => {
|
||||||
|
it('should support short week day', () => {
|
||||||
|
const tpl = '{{ "2024-07-21T20:24:00.000Z" | date: "%a", "Asia/Shanghai" }}'
|
||||||
|
expect(liquid.parseAndRenderSync(tpl)).toEqual('Mon')
|
||||||
|
})
|
||||||
|
it('should support short week day with timezone', () => {
|
||||||
|
const tpl = '{{ "2024-07-21T20:24:00.000Z" | date: "%a", "America/New_York" }}'
|
||||||
|
expect(liquid.parseAndRenderSync(tpl)).toEqual('Sun')
|
||||||
|
})
|
||||||
|
it('should support short week day with locale', () => {
|
||||||
|
const liquid = new Liquid({ locale: 'zh-CN' })
|
||||||
|
const tpl = '{{ "2024-07-21T20:24:00.000Z" | date: "%a", "America/New_York" }}'
|
||||||
|
expect(liquid.parseAndRenderSync(tpl)).toEqual('周日')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('%b', () => {
|
||||||
|
it('should support short month', () => {
|
||||||
|
const tpl = '{{ "2024-07-31T20:24:00.000Z" | date: "%b", "Asia/Shanghai" }}'
|
||||||
|
expect(liquid.parseAndRenderSync(tpl)).toEqual('Aug')
|
||||||
|
})
|
||||||
|
it('should support short week day with locale', () => {
|
||||||
|
const liquid = new Liquid({ locale: 'zh-CN' })
|
||||||
|
const tpl = '{{ "2024-07-31T20:24:00.000Z" | date: "%b", "Asia/Shanghai" }}'
|
||||||
|
expect(liquid.parseAndRenderSync(tpl)).toEqual('8月')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('Intl compatibility', () => {
|
||||||
|
disableIntl()
|
||||||
|
it('should use English if Intl not supported', () => {
|
||||||
|
const liquid = new Liquid()
|
||||||
|
const tpl = '{{ "2024-07-31T20:24:00.000Z" | date: "%b", "Asia/Shanghai" }}'
|
||||||
|
expect(liquid.parseAndRenderSync(tpl)).toEqual('Aug')
|
||||||
|
})
|
||||||
|
it('should use English if Intl not supported even for other locales', () => {
|
||||||
|
const liquid = new Liquid({ locale: 'zh-CN' })
|
||||||
|
const tpl = '{{ "2024-07-31T20:24:00.000Z" | date: "%b", "Asia/Shanghai" }}'
|
||||||
|
expect(liquid.parseAndRenderSync(tpl)).toEqual('Aug')
|
||||||
|
})
|
||||||
|
})
|
||||||
it('should support "now"', function () {
|
it('should support "now"', function () {
|
||||||
// sample: Thursday, February 2, 2023 at 6:25 pm +0000
|
// sample: Thursday, February 2, 2023 at 6:25 pm +0000
|
||||||
return test('{{ "now" | date }}', /\w+, \w+ \d+, \d\d\d\d at \d+:\d\d [ap]m [-+]\d\d\d\d/)
|
return test('{{ "now" | date }}', /\w+, \w+ \d+, \d\d\d\d at \d+:\d\d [ap]m [-+]\d\d\d\d/)
|
||||||
})
|
})
|
||||||
it('should create a new Date when given "now"', function () {
|
|
||||||
return test('{{ "now" | date: "%Y"}}', (new Date()).getFullYear().toString())
|
|
||||||
})
|
|
||||||
it('should create a new Date when given "today"', function () {
|
|
||||||
return test('{{ "today" | date: "%Y"}}', (new Date()).getFullYear().toString())
|
|
||||||
})
|
|
||||||
it('should parse as Date when given a timezoneless string', function () {
|
it('should parse as Date when given a timezoneless string', function () {
|
||||||
return test('{{ "1991-02-22T00:00:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1991-02-22T00:00:00')
|
return test('{{ "1991-02-22T00:00:00" | date: "%Y-%m-%dT%H:%M:%S"}}', '1991-02-22T00:00:00')
|
||||||
})
|
})
|
||||||
@@ -45,14 +104,6 @@ describe('filters/date', function () {
|
|||||||
it('should render object as string', function () {
|
it('should render object as string', function () {
|
||||||
return test('{{ obj | date: "%Y"}}', { obj: {} }, '[object Object]')
|
return test('{{ obj | date: "%Y"}}', { obj: {} }, '[object Object]')
|
||||||
})
|
})
|
||||||
it('should create from number', async function () {
|
|
||||||
const time = new Date('2017-03-07T12:00:00').getTime() / 1000
|
|
||||||
return test('{{ time | date: "%Y-%m-%dT%H:%M:%S" }}', { time }, '2017-03-07T12:00:00')
|
|
||||||
})
|
|
||||||
it('should create from number-like string', async function () {
|
|
||||||
const time = String(new Date('2017-03-07T12:00:00').getTime() / 1000)
|
|
||||||
return test('{{ time | date: "%Y-%m-%dT%H:%M:%S" }}', { time }, '2017-03-07T12:00:00')
|
|
||||||
})
|
|
||||||
it('should support manipulation', async function () {
|
it('should support manipulation', async function () {
|
||||||
return test('{{ date | date: "%s" | minus : 604800 | date: "%Y-%m-%dT%H:%M:%S"}}',
|
return test('{{ date | date: "%s" | minus : 604800 | date: "%Y-%m-%dT%H:%M:%S"}}',
|
||||||
{ date: new Date('2017-03-07T12:00:00') },
|
{ date: new Date('2017-03-07T12:00:00') },
|
||||||
@@ -180,6 +231,10 @@ describe('filters/date_to_string', function () {
|
|||||||
const output = liquid.parseAndRenderSync('{{ "2008-11-07T13:07:54-08:00" | date_to_string: "ordinal", "US" }}')
|
const output = liquid.parseAndRenderSync('{{ "2008-11-07T13:07:54-08:00" | date_to_string: "ordinal", "US" }}')
|
||||||
expect(output).toEqual('Nov 7th, 2008')
|
expect(output).toEqual('Nov 7th, 2008')
|
||||||
})
|
})
|
||||||
|
it('should render none if not valid', function () {
|
||||||
|
const output = liquid.parseAndRenderSync('{{ "hello" | date_to_string: "ordinal", "US" }}')
|
||||||
|
expect(output).toEqual('hello')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('filters/date_to_long_string', function () {
|
describe('filters/date_to_long_string', function () {
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ describe('filters/object', function () {
|
|||||||
})
|
})
|
||||||
it('should inspect cyclic object', () => {
|
it('should inspect cyclic object', () => {
|
||||||
const text = '{{foo | inspect}}'
|
const text = '{{foo | inspect}}'
|
||||||
const foo: any = { bar: 'bar' }
|
const foo: any = { bar: { coo: 'coo' } }
|
||||||
foo.foo = foo
|
foo.foo = foo
|
||||||
const expected = '{"bar":"bar","foo":"[Circular]"}'
|
const expected = '{"bar":{"coo":"coo"},"foo":"[Circular]"}'
|
||||||
return expect(liquid.parseAndRenderSync(text, { foo })).toBe(expected)
|
return expect(liquid.parseAndRenderSync(text, { foo })).toBe(expected)
|
||||||
})
|
})
|
||||||
it('should support space argument', () => {
|
it('should support space argument', () => {
|
||||||
|
|||||||
@@ -243,26 +243,39 @@ describe('filters/string', function () {
|
|||||||
const html = await liquid.parseAndRender('{{ "I\'m not hungry" | number_of_words: "auto"}}')
|
const html = await liquid.parseAndRender('{{ "I\'m not hungry" | number_of_words: "auto"}}')
|
||||||
expect(html).toEqual('3')
|
expect(html).toEqual('3')
|
||||||
})
|
})
|
||||||
|
it('should count words of empty sentence', async () => {
|
||||||
|
const html = await liquid.parseAndRender('{{ "" | number_of_words }}')
|
||||||
|
expect(html).toEqual('0')
|
||||||
|
})
|
||||||
it('should count words of mixed sentence', async () => {
|
it('should count words of mixed sentence', async () => {
|
||||||
const html = await liquid.parseAndRender('{{ "Hello world!" | number_of_words }}')
|
const html = await liquid.parseAndRender('{{ "Hello world!" | number_of_words }}')
|
||||||
expect(html).toEqual('2')
|
expect(html).toEqual('2')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should count words of CJK sentence', async () => {
|
it('should count words of CJK sentence', async () => {
|
||||||
const html = await liquid.parseAndRender('{{ "你好hello世界world" | number_of_words }}')
|
const html = await liquid.parseAndRender('{{ "你好hello世界world" | number_of_words }}')
|
||||||
expect(html).toEqual('1')
|
expect(html).toEqual('1')
|
||||||
})
|
})
|
||||||
|
it('should count words of Latin sentence in CJK mode', async () => {
|
||||||
|
const html = await liquid.parseAndRender('{{ "I\'m not hungry" | number_of_words: "cjk"}}')
|
||||||
|
expect(html).toEqual('3')
|
||||||
|
})
|
||||||
|
it('should count words of empty sentence in CJK mode', async () => {
|
||||||
|
const html = await liquid.parseAndRender('{{ "" | number_of_words: "cjk"}}')
|
||||||
|
expect(html).toEqual('0')
|
||||||
|
})
|
||||||
it('should count words of CJK sentence with mode "cjk"', async () => {
|
it('should count words of CJK sentence with mode "cjk"', async () => {
|
||||||
const html = await liquid.parseAndRender('{{ "你好hello世界world" | number_of_words: "cjk" }}')
|
const html = await liquid.parseAndRender('{{ "你好hello世界world" | number_of_words: "cjk" }}')
|
||||||
expect(html).toEqual('6')
|
expect(html).toEqual('6')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should count words of CJK sentence with mode "auto"', async () => {
|
it('should count words of mixed sentence with mode "auto"', async () => {
|
||||||
const html = await liquid.parseAndRender('{{ "你好hello世界world" | number_of_words: "auto" }}')
|
const html = await liquid.parseAndRender('{{ "你好hello世界world" | number_of_words: "auto" }}')
|
||||||
expect(html).toEqual('6')
|
expect(html).toEqual('6')
|
||||||
})
|
})
|
||||||
|
it('should count words of CJK sentence with mode "auto"', async () => {
|
||||||
|
const html = await liquid.parseAndRender('{{ "你好世界" | number_of_words: "auto" }}')
|
||||||
|
expect(html).toEqual('4')
|
||||||
|
})
|
||||||
it('should handle empty input', async () => {
|
it('should handle empty input', async () => {
|
||||||
const html = await liquid.parseAndRender('{{ "" | number_of_words }}')
|
const html = await liquid.parseAndRender('{{ "" | number_of_words }}')
|
||||||
expect(html).toEqual('0')
|
expect(html).toEqual('0')
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ describe('DoS related', function () {
|
|||||||
await expect(limit10.parseAndRender(src)).rejects.toThrow('template render limit exceeded')
|
await expect(limit10.parseAndRender(src)).rejects.toThrow('template render limit exceeded')
|
||||||
await expect(limit20.parseAndRender(src)).resolves.toBe('1,2,3,4,5,')
|
await expect(limit20.parseAndRender(src)).resolves.toBe('1,2,3,4,5,')
|
||||||
})
|
})
|
||||||
|
it('should support reset when calling render', async () => {
|
||||||
|
const src = '{% for i in (1..5) %}{{i}},{% endfor %}'
|
||||||
|
const liquid = new Liquid({ renderLimit: 0.01 })
|
||||||
|
await expect(liquid.parseAndRender(src)).rejects.toThrow('template render limit exceeded')
|
||||||
|
await expect(liquid.parseAndRender(src, {}, { renderLimit: 1e6 })).resolves.toBe('1,2,3,4,5,')
|
||||||
|
})
|
||||||
it('should take partials into account', async () => {
|
it('should take partials into account', async () => {
|
||||||
mock({
|
mock({
|
||||||
'/small': '{% for i in (1..5) %}{{i}}{% endfor %}',
|
'/small': '{% for i in (1..5) %}{{i}}{% endfor %}',
|
||||||
@@ -50,6 +56,12 @@ describe('DoS related', function () {
|
|||||||
await expect(liquid.parseAndRender('{{ array | slice: 0, 3 | join }}', { array })).resolves.toBe('0 0 0')
|
await expect(liquid.parseAndRender('{{ array | slice: 0, 3 | join }}', { array })).resolves.toBe('0 0 0')
|
||||||
await expect(liquid.parseAndRender('{{ array | slice: 0, 300 | join }}', { array })).rejects.toThrow('memory alloc limit exceeded, line:1, col:1')
|
await expect(liquid.parseAndRender('{{ array | slice: 0, 300 | join }}', { array })).rejects.toThrow('memory alloc limit exceeded, line:1, col:1')
|
||||||
})
|
})
|
||||||
|
it('should support reset when calling render', async () => {
|
||||||
|
const array = Array(1e3).fill(0)
|
||||||
|
const liquid = new Liquid({ memoryLimit: 100 })
|
||||||
|
await expect(liquid.parseAndRender('{{ array | slice: 0, 300 | join }}', { array })).rejects.toThrow('memory alloc limit exceeded, line:1, col:1')
|
||||||
|
await expect(liquid.parseAndRender('{{ array | slice: 0, 300 | join }}', { array }, { memoryLimit: 1e3 })).resolves.toBe(Array(300).fill(0).join(' '))
|
||||||
|
})
|
||||||
it('should throw for too many array iteration in tags', async () => {
|
it('should throw for too many array iteration in tags', async () => {
|
||||||
const array = ['a']
|
const array = ['a']
|
||||||
const liquid = new Liquid({ memoryLimit: 100 })
|
const liquid = new Liquid({ memoryLimit: 100 })
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe('LiquidOptions#fs', function () {
|
|||||||
existsSync: (x: string) => !x.match(/not-exist/),
|
existsSync: (x: string) => !x.match(/not-exist/),
|
||||||
readFile: (x: string) => Promise.resolve(`content for ${x}`),
|
readFile: (x: string) => Promise.resolve(`content for ${x}`),
|
||||||
readFileSync: (x: string) => `content for ${x}`,
|
readFileSync: (x: string) => `content for ${x}`,
|
||||||
fallback: (x: string) => '/root/files/fallback',
|
fallback: (_x: string) => '/root/files/fallback',
|
||||||
resolve: (base: string, path: string) => base + '/' + path
|
resolve: (base: string, path: string) => base + '/' + path
|
||||||
}
|
}
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
@@ -49,7 +49,7 @@ describe('LiquidOptions#fs', function () {
|
|||||||
} as any)
|
} as any)
|
||||||
expect(engine.options.relativeReference).toBe(false)
|
expect(engine.options.relativeReference).toBe(false)
|
||||||
})
|
})
|
||||||
it('should render from in-memory templates', () => {
|
it('should render from in-memory templates', async () => {
|
||||||
const engine = new Liquid({
|
const engine = new Liquid({
|
||||||
templates: {
|
templates: {
|
||||||
entry: '{% layout "main" %}entry',
|
entry: '{% layout "main" %}entry',
|
||||||
@@ -57,6 +57,7 @@ describe('LiquidOptions#fs', function () {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(engine.renderFileSync('entry')).toEqual('header entry footer')
|
expect(engine.renderFileSync('entry')).toEqual('header entry footer')
|
||||||
|
await expect(engine.renderFile('entry')).resolves.toEqual('header entry footer')
|
||||||
})
|
})
|
||||||
it('should render relative in-memory templates', () => {
|
it('should render relative in-memory templates', () => {
|
||||||
const engine = new Liquid({
|
const engine = new Liquid({
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export class DateWithTimezone extends Date {
|
|
||||||
constructor (init: string, timezone: number) {
|
|
||||||
super(init)
|
|
||||||
this.getTimezoneOffset = () => timezone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { LiquidDate } from '../../src/util'
|
||||||
|
|
||||||
|
const locale = Intl.DateTimeFormat().resolvedOptions().locale
|
||||||
|
|
||||||
|
export class DateWithTimezone extends LiquidDate {
|
||||||
|
constructor (init: string, timezone: number) {
|
||||||
|
super(init, locale)
|
||||||
|
this.getTimezoneOffset = () => timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TestDate extends LiquidDate {
|
||||||
|
constructor (v: any) {
|
||||||
|
super(v, locale)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export function disableIntl () {
|
||||||
|
let intl: typeof Intl
|
||||||
|
beforeEach(() => {
|
||||||
|
intl = Intl
|
||||||
|
delete (global as any).Intl
|
||||||
|
})
|
||||||
|
afterEach(() => {
|
||||||
|
(global as any).Intl = intl
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user