mirror of
https://github.com/divkit/divkit.git
synced 2026-06-06 20:07:59 +00:00
Additional datetime funcs
b96996dc12c52fdefb6da21a52aa9ad23b81066a
This commit is contained in:
@@ -12839,6 +12839,7 @@
|
||||
"client/web/divkit/src/utils/flattenAnimation.ts":"divkit/public/client/web/divkit/src/utils/flattenAnimation.ts",
|
||||
"client/web/divkit/src/utils/flattenTransition.ts":"divkit/public/client/web/divkit/src/utils/flattenTransition.ts",
|
||||
"client/web/divkit/src/utils/fontWeightToCss.ts":"divkit/public/client/web/divkit/src/utils/fontWeightToCss.ts",
|
||||
"client/web/divkit/src/utils/formatDate.ts":"divkit/public/client/web/divkit/src/utils/formatDate.ts",
|
||||
"client/web/divkit/src/utils/genClassName.ts":"divkit/public/client/web/divkit/src/utils/genClassName.ts",
|
||||
"client/web/divkit/src/utils/getMarginBox.ts":"divkit/public/client/web/divkit/src/utils/getMarginBox.ts",
|
||||
"client/web/divkit/src/utils/getTouchCoords.ts":"divkit/public/client/web/divkit/src/utils/getTouchCoords.ts",
|
||||
@@ -15094,6 +15095,7 @@
|
||||
"client/web/divkit/tests/utils/flattenAnimation.test.ts":"divkit/public/client/web/divkit/tests/utils/flattenAnimation.test.ts",
|
||||
"client/web/divkit/tests/utils/flattenTransition.test.ts":"divkit/public/client/web/divkit/tests/utils/flattenTransition.test.ts",
|
||||
"client/web/divkit/tests/utils/fontWeightToCss.test.ts":"divkit/public/client/web/divkit/tests/utils/fontWeightToCss.test.ts",
|
||||
"client/web/divkit/tests/utils/formatDate.test.ts":"divkit/public/client/web/divkit/tests/utils/formatDate.test.ts",
|
||||
"client/web/divkit/tests/utils/genClassName.test.ts":"divkit/public/client/web/divkit/tests/utils/genClassName.test.ts",
|
||||
"client/web/divkit/tests/utils/gridCalcTemplates.test.ts":"divkit/public/client/web/divkit/tests/utils/gridCalcTemplates.test.ts",
|
||||
"client/web/divkit/tests/utils/htmlFilter.test.ts":"divkit/public/client/web/divkit/tests/utils/htmlFilter.test.ts",
|
||||
|
||||
@@ -371,6 +371,12 @@ Additional external storage. The `getValue` and `setValue` methods should be imp
|
||||
|
||||
Please note that these methods must be synchronous! DivKit does not have asynchronous render methods, so any `getValue` function cannot be asynchronous. It is possible to process `setValue` asynchronously, but if the value from `getValue` is critical for you and cannot be read synchronously, you must read it before calling `render`.
|
||||
|
||||
#### weekStartDay
|
||||
|
||||
Number, optional. 0 by default.
|
||||
|
||||
Affects the formatting of dates.
|
||||
|
||||
#### theme (DEPRECATED)
|
||||
|
||||
`system` | `light` | `dark`
|
||||
|
||||
Generated
+10
-10
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@divkitframework/divkit",
|
||||
"version": "29.11.0",
|
||||
"version": "30.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@divkitframework/divkit",
|
||||
"version": "29.11.0",
|
||||
"version": "30.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.21.4",
|
||||
@@ -39,7 +39,7 @@
|
||||
"svelte-check": "3.5.0",
|
||||
"svelte-loader": "3.1.9",
|
||||
"svelte-preprocess": "5.0.4",
|
||||
"typescript": "5.0.4",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.91.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4"
|
||||
@@ -13639,16 +13639,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
|
||||
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
@@ -24505,9 +24505,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
|
||||
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"dev": true
|
||||
},
|
||||
"unbox-primitive": {
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"svelte-check": "3.5.0",
|
||||
"svelte-loader": "3.1.9",
|
||||
"svelte-preprocess": "5.0.4",
|
||||
"typescript": "5.0.4",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.91.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4"
|
||||
|
||||
@@ -31,6 +31,7 @@ export function render(opts: {
|
||||
tooltipRoot?: HTMLElement;
|
||||
customComponents?: Map<string, CustomComponentDescription> | undefined;
|
||||
store?: Store;
|
||||
weekStartDay?: number;
|
||||
}): DivkitDebugInstance {
|
||||
const { target, hydrate, ...rest } = opts;
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ export function render(opts: {
|
||||
customComponents?: Map<string, CustomComponentDescription> | undefined;
|
||||
direction?: Direction;
|
||||
store?: Store;
|
||||
weekStartDay?: number;
|
||||
}): DivkitInstance {
|
||||
const { target, hydrate, ...rest } = opts;
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
export let customComponents: Map<string, CustomComponentDescription> | undefined = undefined;
|
||||
export let direction: Direction = 'ltr';
|
||||
export let store: Store | undefined = undefined;
|
||||
export let weekStartDay = 0;
|
||||
|
||||
let isDesktop = writable(platform === 'desktop');
|
||||
if (platform === 'auto' && typeof matchMedia !== 'undefined') {
|
||||
@@ -260,7 +261,7 @@
|
||||
|
||||
const vars = mergeVars(variables, additionalVars);
|
||||
|
||||
const prepared = prepareVars(jsonProp, logError, store);
|
||||
const prepared = prepareVars(jsonProp, logError, store, weekStartDay);
|
||||
if (!prepared.vars.length) {
|
||||
if (prepared.hasExpression) {
|
||||
return constStore(prepared.applyVars(vars));
|
||||
@@ -280,7 +281,7 @@
|
||||
additionalVars?: Map<string, Variable>,
|
||||
keepComplex = false
|
||||
): MaybeMissing<T> {
|
||||
const prepared = prepareVars(jsonProp, logError, store);
|
||||
const prepared = prepareVars(jsonProp, logError, store, weekStartDay);
|
||||
|
||||
if (!prepared.hasExpression) {
|
||||
return jsonProp;
|
||||
@@ -1589,7 +1590,9 @@
|
||||
const stores = exprVars.map(name => variables.get(name) || awaitVariableChanges(name));
|
||||
|
||||
derived(stores, () => {
|
||||
const res = evalExpression(variables, store, ast);
|
||||
const res = evalExpression(variables, store, ast, {
|
||||
weekStartDay
|
||||
});
|
||||
|
||||
res.warnings.forEach(logError);
|
||||
|
||||
|
||||
@@ -6,15 +6,143 @@ import Lottie from 'lottie-web/build/player/lottie';
|
||||
import { initComponents } from './devCustomComponents';
|
||||
|
||||
const json = {
|
||||
"templates": {},
|
||||
"templates": {
|
||||
"tutorialCard": {
|
||||
"type": "container",
|
||||
"border": {
|
||||
"corner_radius": 16,
|
||||
"stroke": {
|
||||
"color": "#aaaaaa",
|
||||
"width": 1
|
||||
}
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"type": "text",
|
||||
"font_size": 21,
|
||||
"font_weight": "bold",
|
||||
"margins": {
|
||||
"bottom": 8
|
||||
},
|
||||
"$text": "title"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"font_size": 16,
|
||||
"margins": {
|
||||
"bottom": 4
|
||||
},
|
||||
"$text": "body"
|
||||
},
|
||||
{
|
||||
"type": "container",
|
||||
"$items": "links"
|
||||
}
|
||||
],
|
||||
"margins": {
|
||||
"bottom": 6
|
||||
},
|
||||
"orientation": "vertical",
|
||||
"paddings": {
|
||||
"top": 12,
|
||||
"bottom": 12,
|
||||
"left": 12,
|
||||
"right": 12
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"type": "text",
|
||||
"action": {
|
||||
"$url": "link",
|
||||
"$log_id": "log"
|
||||
},
|
||||
"font_size": 14,
|
||||
"margins": {
|
||||
"bottom": 2
|
||||
},
|
||||
"text_color": "#0000ff",
|
||||
"underline": "single",
|
||||
"$text": "link_text"
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
"log_id": "snapshot_test_card",
|
||||
"log_id": "div2_sample_card",
|
||||
"states": [
|
||||
{
|
||||
"state_id": 0,
|
||||
"div": {
|
||||
"type": "text",
|
||||
"text": "Hello world"
|
||||
"type": "container",
|
||||
"items": [
|
||||
{
|
||||
"type": "tutorialCard",
|
||||
"title": "@{getHours(parseUnixTimeAsLocal(1715855877))}",
|
||||
"body": "Server side driven ui Яндекса\nПолезные ссылки:",
|
||||
"links": [
|
||||
{
|
||||
"type": "link",
|
||||
"link_text": "Официальная документация",
|
||||
"link": "https://doc.yandex-team.ru/divkit/overview/",
|
||||
"log": "docs"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"link_text": "Примеры карточек",
|
||||
"link": "https://a.yandex-team.ru/arcadia/divkit/public/test_data/samples",
|
||||
"log": "samples"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"link_text": "Клуб в Этушке",
|
||||
"link": "https://clubs.at.yandex-team.ru/divkit/",
|
||||
"log": "atushka"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"link_text": "Канал в Мессенджере",
|
||||
"link": "https://q.yandex-team.ru/#/join/b7263a0a-c2ef-4c02-9669-e8a1f4cf3762",
|
||||
"log": "slack"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "tutorialCard",
|
||||
"title": "Горячие клавиши редактора",
|
||||
"body": "ctrl+space - вызвать автодополнение\nctrl+f2 - заменить всё\nctrl+shift+i - отформатировать json\nctrl+shift+a закомментировать блок\nСхолпнуть json-блоки можно с помощью стрелочек, что появляются при наведении на блок с номерами строк слева\nf1 - вызвать меню со всеми доступными командами и горячими клавишами",
|
||||
"links": [
|
||||
{
|
||||
"type": "separator",
|
||||
"delimiter_style": {
|
||||
"color": "#00000000"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "tutorialCard",
|
||||
"title": "Интерактивная верстка на ваших устройствах",
|
||||
"body": "Для того, чтобы посмотреть вашу верстку на android:\n1) Скачайте demo apk по ссылке ниже\n2) Установите apk на устройство и перейдите во вкладку \"Div2\"\n3) Нажмите на share на этой страничке и отсканируйте QR-код, нажав соответствующую кнопку в приложении, либо вставьте урл из поля \"Link to json\"\n4) Для каждого подключенного устройства в редакторе появится вкладка с тем, что отображается сейчас на экране",
|
||||
"links": [
|
||||
{
|
||||
"type": "link",
|
||||
"link_text": "Android демо-приложение",
|
||||
"link": "https://beta.m.soft.yandex.ru/description?app=divkit&platform_shortcut=android&branch=unknown",
|
||||
"log": "android"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"link_text": "iOS демо-приложение",
|
||||
"link": "https://teamcity.yandex-team.ru/buildConfiguration/MobileNew_Monorepo_DivKit_iOS_trunk?branch=&mode=builds",
|
||||
"log": "ios"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"margins": {
|
||||
"top": 8,
|
||||
"bottom": 8,
|
||||
"left": 8,
|
||||
"right": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -100,6 +100,7 @@ export interface EvalContext {
|
||||
warnings: WrappedError[];
|
||||
safeIntegerOverflow: boolean;
|
||||
store?: Store;
|
||||
weekStartDay: number;
|
||||
}
|
||||
|
||||
register();
|
||||
@@ -560,7 +561,9 @@ export function evalAny(ctx: EvalContext, expr: Node): EvalValue {
|
||||
throw new Error('Unsupported expression');
|
||||
}
|
||||
|
||||
export function evalExpression(vars: VariablesMap, store: Store | undefined, expr: Node): {
|
||||
export function evalExpression(vars: VariablesMap, store: Store | undefined, expr: Node, opts?: {
|
||||
weekStartDay?: number;
|
||||
}): {
|
||||
result: EvalResult;
|
||||
warnings: WrappedError[];
|
||||
} {
|
||||
@@ -569,7 +572,8 @@ export function evalExpression(vars: VariablesMap, store: Store | undefined, exp
|
||||
variables: vars,
|
||||
warnings: [],
|
||||
safeIntegerOverflow: false,
|
||||
store
|
||||
store,
|
||||
weekStartDay: opts?.weekStartDay || 0
|
||||
};
|
||||
|
||||
const result = evalAny(ctx, expr);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { DatetimeValue, EvalContext, EvalValue, IntegerValue } from '../eval';
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import type { DatetimeValue, EvalContext, EvalValue, IntegerValue, StringValue } from '../eval';
|
||||
import { registerFunc } from './funcs';
|
||||
import { DATETIME, INTEGER } from '../const';
|
||||
import { DATETIME, INTEGER, STRING } from '../const';
|
||||
import { valToString } from '../utils';
|
||||
import { toBigInt } from '../bigint';
|
||||
import { formatDate } from '../../utils/formatDate';
|
||||
|
||||
function getMaxDate(date: Date): number {
|
||||
const copy = new Date(date);
|
||||
@@ -20,6 +22,17 @@ function parseUnixTime(_ctx: EvalContext, arg: IntegerValue): EvalValue {
|
||||
};
|
||||
}
|
||||
|
||||
function parseUnixTimeAsLocal(_ctx: EvalContext, arg: IntegerValue): EvalValue {
|
||||
const date = new Date(Number(arg.value) * 1000);
|
||||
const offset = date.getTimezoneOffset();
|
||||
date.setMinutes(date.getMinutes() - offset);
|
||||
|
||||
return {
|
||||
type: DATETIME,
|
||||
value: date
|
||||
};
|
||||
}
|
||||
|
||||
function nowLocal(): EvalValue {
|
||||
return {
|
||||
type: DATETIME,
|
||||
@@ -164,6 +177,19 @@ const getter = (
|
||||
};
|
||||
};
|
||||
|
||||
function makeFormat(isUTC: boolean) {
|
||||
return (ctx: EvalContext, datetime: DatetimeValue, format: StringValue, locale?: StringValue): EvalValue => {
|
||||
return {
|
||||
type: STRING,
|
||||
value: formatDate(datetime.value, format.value, {
|
||||
locale: locale?.value,
|
||||
isUTC,
|
||||
weekStartDay: ctx.weekStartDay
|
||||
})
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const getYear = getter('getUTCFullYear');
|
||||
const getMonth = getter('getUTCMonth');
|
||||
const getDay = getter('getUTCDate');
|
||||
@@ -173,8 +199,12 @@ const getMinutes = getter('getUTCMinutes');
|
||||
const getSeconds = getter('getUTCSeconds');
|
||||
const getMillis = getter('getUTCMilliseconds');
|
||||
|
||||
const formatAsLocal = makeFormat(false);
|
||||
const formatAsUTC = makeFormat(true);
|
||||
|
||||
export function registerDatetime(): void {
|
||||
registerFunc('parseUnixTime', [INTEGER], parseUnixTime);
|
||||
registerFunc('parseUnixTimeAsLocal', [INTEGER], parseUnixTimeAsLocal);
|
||||
registerFunc('nowLocal', [], nowLocal);
|
||||
registerFunc('addMillis', [DATETIME, INTEGER], addMillis);
|
||||
|
||||
@@ -194,4 +224,9 @@ export function registerDatetime(): void {
|
||||
registerFunc('getMinutes', [DATETIME], getMinutes);
|
||||
registerFunc('getSeconds', [DATETIME], getSeconds);
|
||||
registerFunc('getMillis', [DATETIME], getMillis);
|
||||
|
||||
registerFunc('formatDateAsLocal', [DATETIME, STRING], formatAsLocal);
|
||||
registerFunc('formatDateAsUTC', [DATETIME, STRING], formatAsUTC);
|
||||
registerFunc('formatDateAsLocalWithLocale', [DATETIME, STRING, STRING], formatAsLocal);
|
||||
registerFunc('formatDateAsUTCWithLocale', [DATETIME, STRING, STRING], formatAsUTC);
|
||||
}
|
||||
|
||||
@@ -23,13 +23,24 @@ class ExpressionBinding {
|
||||
* @param logError
|
||||
*/
|
||||
apply(
|
||||
variables: VariablesMap,
|
||||
logError: LogError,
|
||||
store: Store | undefined,
|
||||
keepComplex = false
|
||||
{
|
||||
variables,
|
||||
logError,
|
||||
store,
|
||||
weekStartDay,
|
||||
keepComplex
|
||||
}: {
|
||||
variables: VariablesMap;
|
||||
logError: LogError;
|
||||
store: Store | undefined;
|
||||
weekStartDay: number;
|
||||
keepComplex?: boolean;
|
||||
}
|
||||
): VariableValue | string | undefined {
|
||||
try {
|
||||
const res = evalExpression(variables, store, this.ast);
|
||||
const res = evalExpression(variables, store, this.ast, {
|
||||
weekStartDay
|
||||
});
|
||||
res.warnings.forEach(logError);
|
||||
const result = res.result;
|
||||
|
||||
@@ -180,28 +191,31 @@ function prepareVarsObj<T>(
|
||||
|
||||
function applyVars(
|
||||
jsonProp: unknown,
|
||||
variables: VariablesMap,
|
||||
logError: LogError,
|
||||
store: Store | undefined,
|
||||
keepComplex = false
|
||||
opts: {
|
||||
variables: VariablesMap;
|
||||
logError: LogError;
|
||||
store: Store | undefined;
|
||||
weekStartDay: number;
|
||||
keepComplex?: boolean;
|
||||
}
|
||||
): unknown {
|
||||
if (jsonProp) {
|
||||
if (
|
||||
(process.env.ENABLE_EXPRESSIONS || process.env.ENABLE_EXPRESSIONS === undefined) &&
|
||||
jsonProp instanceof ExpressionBinding
|
||||
) {
|
||||
return jsonProp.apply(variables, logError, store, keepComplex);
|
||||
return jsonProp.apply(opts);
|
||||
} else if (
|
||||
(!process.env.ENABLE_EXPRESSIONS && process.env.ENABLE_EXPRESSIONS !== undefined) &&
|
||||
jsonProp instanceof VariableBinding
|
||||
) {
|
||||
return jsonProp.apply(variables, logError);
|
||||
return jsonProp.apply(opts.variables, opts.logError);
|
||||
} else if (Array.isArray(jsonProp)) {
|
||||
return jsonProp.map(it => applyVars(it, variables, logError, store));
|
||||
return jsonProp.map(it => applyVars(it, opts));
|
||||
} else if (typeof jsonProp === 'object') {
|
||||
const res: Record<string, unknown> = {};
|
||||
for (const key in jsonProp) {
|
||||
res[key] = applyVars(jsonProp[key as keyof typeof jsonProp], variables, logError, store);
|
||||
res[key] = applyVars(jsonProp[key as keyof typeof jsonProp], opts);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -209,7 +223,7 @@ function applyVars(
|
||||
return jsonProp;
|
||||
}
|
||||
|
||||
export function prepareVars<T>(jsonProp: T, logError: LogError, store: Store | undefined): {
|
||||
export function prepareVars<T>(jsonProp: T, logError: LogError, store: Store | undefined, weekStartDay: number): {
|
||||
vars: string[];
|
||||
hasExpression: boolean;
|
||||
applyVars: (variables: VariablesMap, keepComplex?: boolean) => MaybeMissing<T>;
|
||||
@@ -229,7 +243,13 @@ export function prepareVars<T>(jsonProp: T, logError: LogError, store: Store | u
|
||||
vars,
|
||||
hasExpression: result.hasExpression,
|
||||
applyVars(variables: VariablesMap, keepComplex?: boolean) {
|
||||
return applyVars(root, variables, logError, store, keepComplex) as MaybeMissing<T>;
|
||||
return applyVars(root, {
|
||||
variables,
|
||||
logError,
|
||||
store,
|
||||
weekStartDay,
|
||||
keepComplex
|
||||
}) as MaybeMissing<T>;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ export function render(opts: {
|
||||
customComponents?: Map<string, CustomComponentDescription> | undefined;
|
||||
direction?: Direction;
|
||||
store?: Store;
|
||||
weekStartDay?: number;
|
||||
}) {
|
||||
// Root has client-side typings, not a server one
|
||||
return (Root as any).render(opts).html;
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
// Some code based on the https://github.com/date-fns (MIT License)
|
||||
|
||||
type FormatGetter = (opts: Intl.DateTimeFormatOptions & {
|
||||
date?: Date;
|
||||
}, field: Intl.DateTimeFormatPartTypes | 'week' | 'weekyear' | 'extendedyear' | 'weekofmonth' | 'dayofyear' | 'dayofweekinmonth' | 'weekdaynumeric') => string | undefined;
|
||||
|
||||
function formatNumber(num: string | undefined, len: number | undefined): string | undefined {
|
||||
if (!num || !len) {
|
||||
return num;
|
||||
}
|
||||
|
||||
return num.padStart(len, '0');
|
||||
}
|
||||
|
||||
const formatters: Record<string, (token: number, getter: FormatGetter) => string | undefined> = {
|
||||
G(token, getter) {
|
||||
let era: Intl.DateTimeFormatOptions['era'];
|
||||
if (token < 4) {
|
||||
era = 'short';
|
||||
} else if (token === 5) {
|
||||
era = 'narrow';
|
||||
} else {
|
||||
era = 'long';
|
||||
}
|
||||
return getter({
|
||||
era
|
||||
}, 'era');
|
||||
},
|
||||
d(token, getter) {
|
||||
return formatNumber(getter({
|
||||
day: 'numeric'
|
||||
}, 'day'), token > 1 ? token : 0);
|
||||
},
|
||||
D(token, getter) {
|
||||
return formatNumber(getter({}, 'dayofyear'), token > 1 ? token : 0);
|
||||
},
|
||||
F(token, getter) {
|
||||
return formatNumber(getter({}, 'dayofweekinmonth'), token > 1 ? token : 0);
|
||||
},
|
||||
M(token, getter) {
|
||||
let month: Intl.DateTimeFormatOptions['month'];
|
||||
if (token === 1) {
|
||||
month = 'numeric';
|
||||
} else if (token === 2) {
|
||||
month = '2-digit';
|
||||
} else if (token === 3) {
|
||||
month = 'short';
|
||||
} else if (token === 5) {
|
||||
month = 'narrow';
|
||||
} else {
|
||||
month = 'long';
|
||||
}
|
||||
return getter({
|
||||
month,
|
||||
// to get a genitive case of month
|
||||
day: 'numeric'
|
||||
}, 'month');
|
||||
},
|
||||
y(token, getter) {
|
||||
return formatNumber(getter({
|
||||
year: token === 2 ? '2-digit' : 'numeric'
|
||||
}, 'year'), token > 2 ? token : undefined);
|
||||
},
|
||||
Y(token, getter) {
|
||||
return formatNumber(getter({
|
||||
year: token === 2 ? '2-digit' : 'numeric'
|
||||
}, 'weekyear'), token > 2 ? token : undefined);
|
||||
},
|
||||
u(token, getter) {
|
||||
return formatNumber(getter({
|
||||
year: 'numeric'
|
||||
}, 'extendedyear'), token > 1 ? token : undefined);
|
||||
},
|
||||
E(token, getter) {
|
||||
let weekday: Intl.DateTimeFormatOptions['weekday'];
|
||||
if (token <= 3) {
|
||||
weekday = 'short';
|
||||
} else if (token === 5) {
|
||||
weekday = 'narrow';
|
||||
} else {
|
||||
weekday = 'long';
|
||||
}
|
||||
return getter({
|
||||
weekday
|
||||
}, 'weekday');
|
||||
},
|
||||
e(token, getter) {
|
||||
if (token > 2) {
|
||||
return formatters.E(token, getter);
|
||||
}
|
||||
return formatNumber(getter({}, 'weekdaynumeric'), token > 1 ? token : undefined);
|
||||
},
|
||||
w(token, getter) {
|
||||
return formatNumber(getter({}, 'week'), token > 1 ? token : undefined);
|
||||
},
|
||||
W(token, getter) {
|
||||
return formatNumber(getter({}, 'weekofmonth'), token > 1 ? token : undefined);
|
||||
},
|
||||
H(token, getter) {
|
||||
const hours = getter({
|
||||
hour: 'numeric',
|
||||
hour12: false,
|
||||
hourCycle: 'h23',
|
||||
}, 'hour');
|
||||
|
||||
if (!hours) {
|
||||
return;
|
||||
}
|
||||
|
||||
// v8 may return '24' with 'h23' style
|
||||
const fixed = String(Number(hours) % 24);
|
||||
return formatNumber(fixed, token > 1 ? token : undefined);
|
||||
},
|
||||
h(token, getter) {
|
||||
return formatNumber(getter({
|
||||
hour: 'numeric',
|
||||
hour12: true,
|
||||
hourCycle: 'h12',
|
||||
}, 'hour'), token > 1 ? token : undefined);
|
||||
},
|
||||
K(token, getter) {
|
||||
const hours = getter({
|
||||
hour: 'numeric',
|
||||
hour12: true,
|
||||
hourCycle: 'h11',
|
||||
}, 'hour');
|
||||
|
||||
if (!hours) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fixed = String(Number(hours) % 12);
|
||||
return formatNumber(fixed, token > 1 ? token : undefined);
|
||||
},
|
||||
k(token, getter) {
|
||||
return formatNumber(getter({
|
||||
hour: 'numeric',
|
||||
hour12: false,
|
||||
hourCycle: 'h24',
|
||||
}, 'hour'), token > 2 ? token : undefined);
|
||||
},
|
||||
a(_token, getter) {
|
||||
return getter({
|
||||
hour: 'numeric',
|
||||
hour12: true,
|
||||
hourCycle: 'h11',
|
||||
dayPeriod: undefined
|
||||
}, 'dayPeriod');
|
||||
},
|
||||
m(token, getter) {
|
||||
return formatNumber(getter({
|
||||
minute: 'numeric'
|
||||
}, 'minute'), token > 1 ? token : undefined);
|
||||
},
|
||||
s(token, getter) {
|
||||
return formatNumber(getter({
|
||||
second: 'numeric'
|
||||
}, 'second'), token > 1 ? token : undefined);
|
||||
},
|
||||
S(token, getter) {
|
||||
const res = getter({
|
||||
fractionalSecondDigits: Math.min(3, token) as 1 | 2 | 3
|
||||
}, 'fractionalSecond');
|
||||
|
||||
if (res && token > 3) {
|
||||
return res.padEnd(token, '0');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
const formattingTokensRegExp =
|
||||
/(\w)\1*|''|'(''|[^'])+('|$)|./g;
|
||||
|
||||
const escapedStringRegExp = /^'([^]*?)'?$/;
|
||||
const doubleQuoteRegExp = /''/g;
|
||||
const unescapedLatinCharacterRegExp = /[a-zA-Z]/;
|
||||
|
||||
const msInDay = 1000 * 60 * 60 * 24;
|
||||
|
||||
function cleanEscapedString(input: string): string {
|
||||
const matched = input.match(escapedStringRegExp);
|
||||
|
||||
if (!matched) {
|
||||
return input;
|
||||
}
|
||||
|
||||
return matched[1].replace(doubleQuoteRegExp, "'");
|
||||
}
|
||||
|
||||
function getWeekFirstDay(date: Date, isUTC: boolean, weekStartDay: number): Date {
|
||||
const weekDay = date[isUTC ? 'getUTCDay' : 'getDay']();
|
||||
|
||||
// Monday logic
|
||||
const offset = (weekDay < weekStartDay) ?
|
||||
weekStartDay - weekDay - 7 :
|
||||
weekStartDay - weekDay;
|
||||
|
||||
return new Date(date.getTime() + msInDay * offset);
|
||||
}
|
||||
|
||||
function getFirstWeekFirstDay(date: Date, isUTC: boolean, weekStartDay: number): Date {
|
||||
const firstDay = new Date(date);
|
||||
firstDay[isUTC ? 'setUTCDate' : 'setDate'](1);
|
||||
firstDay[isUTC ? 'setUTCMonth' : 'setMonth'](0);
|
||||
|
||||
return getWeekFirstDay(firstDay, isUTC, weekStartDay);
|
||||
}
|
||||
|
||||
function dayDiff(date0: Date, date1: Date): number {
|
||||
return Math.round((date0.getTime() - date1.getTime()) / msInDay);
|
||||
}
|
||||
|
||||
function getIOSWeekYear(date: Date, isUTC: boolean, weekStartDay: number): {
|
||||
week: number;
|
||||
year: number;
|
||||
} {
|
||||
let week = 0;
|
||||
|
||||
const firstWeekFirstDay = getFirstWeekFirstDay(date, isUTC || false, weekStartDay);
|
||||
const nextYearDate = new Date(date);
|
||||
nextYearDate[isUTC ? 'setUTCFullYear' : 'setFullYear'](date[isUTC ? 'getUTCFullYear' : 'getFullYear']() + 1);
|
||||
const nextYearFirstWeekFirstDay = getFirstWeekFirstDay(nextYearDate, isUTC || false, weekStartDay);
|
||||
|
||||
const isBeforeFirstWeek = date.getTime() < firstWeekFirstDay.getTime();
|
||||
const isAfterLastWeek = date.getTime() >= nextYearFirstWeekFirstDay.getTime();
|
||||
|
||||
let year = date[isUTC ? 'getUTCFullYear' : 'getFullYear']();
|
||||
if (isBeforeFirstWeek) {
|
||||
--year;
|
||||
firstWeekFirstDay[isUTC ? 'setUTCFullYear' : 'setFullYear'](firstWeekFirstDay[isUTC ? 'getUTCFullYear' : 'getFullYear']() - 1);
|
||||
const dayOfYear = dayDiff(getWeekFirstDay(date, isUTC, weekStartDay), firstWeekFirstDay);
|
||||
week = Math.round(dayOfYear / 7) + 1;
|
||||
} else if (isAfterLastWeek) {
|
||||
++year;
|
||||
week = 1;
|
||||
} else {
|
||||
const dayOfYear = dayDiff(getWeekFirstDay(date, isUTC, weekStartDay), firstWeekFirstDay);
|
||||
week = Math.round(dayOfYear / 7) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
week,
|
||||
year
|
||||
};
|
||||
}
|
||||
|
||||
export function formatDate(date: Date, format: string, {
|
||||
locale,
|
||||
isUTC,
|
||||
weekStartDay = 0
|
||||
}: {
|
||||
locale?: string;
|
||||
isUTC?: boolean;
|
||||
weekStartDay?: number;
|
||||
} = {}): string {
|
||||
const getter: FormatGetter = (opts, field): string | undefined => {
|
||||
if (field === 'week') {
|
||||
const { week } = getIOSWeekYear(date, isUTC || false, weekStartDay);
|
||||
|
||||
return String(week);
|
||||
}
|
||||
|
||||
if (field === 'weekofmonth') {
|
||||
const weekday = date[isUTC ? 'getUTCDay' : 'getDay']();
|
||||
const firstDay = new Date(date);
|
||||
firstDay[isUTC ? 'setUTCDate' : 'setDate'](1);
|
||||
const firstWeekday = firstDay[isUTC ? 'getUTCDay' : 'getDay']();
|
||||
const dayOfMonth = date[isUTC ? 'getUTCDate' : 'getDate']();
|
||||
|
||||
return String(Math.ceil(dayOfMonth / 7) + (weekday < firstWeekday ? 1 : 0));
|
||||
}
|
||||
|
||||
if (field === 'dayofweekinmonth') {
|
||||
const dayOfMonth = date[isUTC ? 'getUTCDate' : 'getDate']();
|
||||
|
||||
return String(Math.ceil(dayOfMonth / 7));
|
||||
}
|
||||
|
||||
if (field === 'weekdaynumeric') {
|
||||
let weekday = date[isUTC ? 'getUTCDay' : 'getDay']();
|
||||
|
||||
if (weekday < weekStartDay) {
|
||||
weekday += 7;
|
||||
}
|
||||
|
||||
return String(weekday - weekStartDay + 1);
|
||||
}
|
||||
|
||||
if (field === 'dayofyear') {
|
||||
const firstDay = new Date(date);
|
||||
firstDay[isUTC ? 'setUTCMonth' : 'setMonth'](0);
|
||||
firstDay[isUTC ? 'setUTCDate' : 'setDate'](1);
|
||||
firstDay[isUTC ? 'setUTCHours' : 'setHours'](1);
|
||||
firstDay[isUTC ? 'setUTCMinutes' : 'setMinutes'](1);
|
||||
firstDay[isUTC ? 'setUTCSeconds' : 'setSeconds'](1);
|
||||
|
||||
const dayOfYear = Math.ceil((date.getTime() - firstDay.getTime()) / msInDay);
|
||||
|
||||
return String(dayOfYear);
|
||||
}
|
||||
|
||||
if (field === 'weekyear') {
|
||||
let { year } = getIOSWeekYear(date, isUTC || false, weekStartDay);
|
||||
|
||||
if (year < 1) {
|
||||
year = 1 - year;
|
||||
}
|
||||
|
||||
if (opts.year === '2-digit') {
|
||||
return String(year % 100);
|
||||
}
|
||||
return String(year);
|
||||
}
|
||||
|
||||
if (field === 'extendedyear') {
|
||||
const year = date[isUTC ? 'getUTCFullYear' : 'getFullYear']();
|
||||
|
||||
if (opts.year === '2-digit') {
|
||||
return String(year % 100);
|
||||
}
|
||||
return String(year);
|
||||
}
|
||||
|
||||
if (isUTC) {
|
||||
opts.timeZone = 'UTC';
|
||||
}
|
||||
|
||||
const formatter = new Intl.DateTimeFormat(locale, opts);
|
||||
const parts = formatter.formatToParts(date);
|
||||
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (parts[i].type === field) {
|
||||
return parts[i].value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (format
|
||||
.match(formattingTokensRegExp) || [])
|
||||
.map(substring => {
|
||||
if (substring === "''") {
|
||||
return "'";
|
||||
}
|
||||
|
||||
const firstCharacter = substring[0];
|
||||
if (firstCharacter === "'") {
|
||||
return cleanEscapedString(substring);
|
||||
}
|
||||
|
||||
if (firstCharacter === 'z' || firstCharacter === 'Z') {
|
||||
throw new Error(`z/Z not supported in [${format}].`);
|
||||
}
|
||||
|
||||
if (formatters[firstCharacter]) {
|
||||
return formatters[firstCharacter](substring.length, getter);
|
||||
}
|
||||
|
||||
if (firstCharacter.match(unescapedLatinCharacterRegExp)) {
|
||||
throw new Error(
|
||||
`Format string contains an unescaped latin alphabet character "${firstCharacter}"`
|
||||
);
|
||||
}
|
||||
|
||||
return substring;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
@@ -55,7 +55,9 @@ function runCase(item: any) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const res = evalExpression(vars, undefined, ast);
|
||||
const res = evalExpression(vars, undefined, ast, {
|
||||
weekStartDay: item.platform_specific?.web?.weekStartDay || 0
|
||||
});
|
||||
if (item.expected.value !== '' || res.result.type !== 'error') {
|
||||
if (res.result.type === 'number' && item.expected.type === 'number') {
|
||||
expect(res.result.value).toBeCloseTo(item.expected.value);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ describe('correctPositiveNumber', () => {
|
||||
test('no vars', () => {
|
||||
const prepared = prepareVars({
|
||||
prop: 123
|
||||
}, logError, undefined);
|
||||
}, logError, undefined, 0);
|
||||
expect(prepared.vars).toStrictEqual([]);
|
||||
expect(prepared.applyVars(new Map())).toStrictEqual({
|
||||
prop: 123
|
||||
@@ -24,7 +24,7 @@ describe('correctPositiveNumber', () => {
|
||||
test('simple', () => {
|
||||
const prepared = prepareVars({
|
||||
prop: '@{var}'
|
||||
}, logError, undefined);
|
||||
}, logError, undefined, 0);
|
||||
expect(prepared.vars).toStrictEqual(['var']);
|
||||
const map = new Map();
|
||||
const variable = new IntegerVariable('var', 123);
|
||||
@@ -38,7 +38,7 @@ describe('correctPositiveNumber', () => {
|
||||
test('simple no val', () => {
|
||||
const prepared = prepareVars({
|
||||
prop: '@{var}'
|
||||
}, noError, undefined);
|
||||
}, noError, undefined, 0);
|
||||
expect(prepared.vars).toStrictEqual(['var']);
|
||||
const map = new Map();
|
||||
expect(prepared.applyVars(map)).toStrictEqual({
|
||||
@@ -53,7 +53,7 @@ describe('correctPositiveNumber', () => {
|
||||
'smth',
|
||||
456
|
||||
]
|
||||
}, logError, undefined);
|
||||
}, logError, undefined, 0);
|
||||
expect(prepared.vars).toStrictEqual(['var']);
|
||||
const map = new Map();
|
||||
const variable = new IntegerVariable('var', 123);
|
||||
@@ -70,7 +70,7 @@ describe('correctPositiveNumber', () => {
|
||||
test('interpolation', () => {
|
||||
const prepared = prepareVars({
|
||||
prop: 'Hello, @{var}'
|
||||
}, logError, undefined);
|
||||
}, logError, undefined, 0);
|
||||
expect(prepared.vars).toStrictEqual(['var']);
|
||||
const map = new Map();
|
||||
const variable = new IntegerVariable('var', 123);
|
||||
@@ -83,7 +83,7 @@ describe('correctPositiveNumber', () => {
|
||||
test('multiple vars', () => {
|
||||
const prepared = prepareVars({
|
||||
prop: 'Hello, @{var}. Good bye, @{var2}'
|
||||
}, logError, undefined);
|
||||
}, logError, undefined, 0);
|
||||
expect(prepared.vars).toStrictEqual(['var', 'var2']);
|
||||
const map = new Map();
|
||||
const variable = new IntegerVariable('var', 123);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"target": "ES2021",
|
||||
"strict": true,
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
@@ -15,6 +15,7 @@ import type {
|
||||
import type { GlobalVariablesController, Variable } from './variables';
|
||||
import type { WrappedError } from './common';
|
||||
import type { CustomComponentDescription } from './custom';
|
||||
import type { Store } from './store';
|
||||
|
||||
export interface DivkitDebugInstance extends DivkitInstance {
|
||||
getDebugVariables(): Map<string, Variable>;
|
||||
@@ -42,6 +43,8 @@ export function render(opts: {
|
||||
fetchInit?: FetchInit;
|
||||
tooltipRoot?: HTMLElement;
|
||||
customComponents?: Map<string, CustomComponentDescription> | undefined;
|
||||
store?: Store;
|
||||
weekStartDay?: number;
|
||||
}): DivkitDebugInstance;
|
||||
|
||||
export { createVariable, createGlobalVariablesController } from './variables';
|
||||
|
||||
@@ -36,6 +36,7 @@ export function render(opts: {
|
||||
tooltipRoot?: HTMLElement;
|
||||
customComponents?: Map<string, CustomComponentDescription> | undefined;
|
||||
store?: Store;
|
||||
weekStartDay?: number;
|
||||
}): DivkitInstance;
|
||||
|
||||
export { createVariable, createGlobalVariablesController } from './variables';
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ export function render(opts: {
|
||||
tooltipRoot?: HTMLElement;
|
||||
customComponents?: Map<string, CustomComponentDescription> | undefined;
|
||||
store?: Store;
|
||||
weekStartDay?: number;
|
||||
}): DivkitInstance;
|
||||
|
||||
export { createVariable, createGlobalVariablesController } from './variables';
|
||||
|
||||
+1
@@ -19,4 +19,5 @@ export function render(opts: {
|
||||
typefaceProvider?: TypefaceProvider;
|
||||
customComponents?: Map<string, CustomComponentDescription> | undefined;
|
||||
store?: Store;
|
||||
weekStartDay?: number;
|
||||
}): string;
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
"result_type": "datetime",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -366,7 +367,8 @@
|
||||
"result_type": "string",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -386,7 +388,8 @@
|
||||
"result_type": "string",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -410,7 +413,8 @@
|
||||
"result_type": "string",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -434,7 +438,8 @@
|
||||
"result_type": "string",
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -21,7 +22,8 @@
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -33,7 +35,8 @@
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -45,10 +48,11 @@
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
{
|
||||
"name": "formatDateAsUTCWithLocale(parseUnixTime(0), en-US) => 0:00 AM",
|
||||
"expression": "@{formatDateAsUTCWithLocale(parseUnixTime(0), 'K:mm a', 'en-US')}",
|
||||
"expected": {
|
||||
@@ -57,10 +61,11 @@
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
{
|
||||
"name": "formatDateAsUTCWithLocale(setDay(parseUnixTime(0), 13), en-US) => 1970-01-01T00:00:00.000",
|
||||
"expression": "@{formatDateAsUTCWithLocale(setDay(parseUnixTime(0), 13), 'yyyy-MM-dd\\'T\\'HH:mm:ss.SSS', 'en-US')}",
|
||||
"expected": {
|
||||
@@ -69,7 +74,8 @@
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -81,19 +87,26 @@
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "formatDateAsUTCWithLocale(parseUnixTime(3927600), 'ru-RU') => 1970-02-03, week of year 7",
|
||||
"expression": "@{formatDateAsUTCWithLocale(parseUnixTime(3927600), 'yyyy-MM-dd\\', week of year \\'w', 'ru-RU')}",
|
||||
"platform_specific": {
|
||||
"web": {
|
||||
"weekStartDay": 1
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"type": "string",
|
||||
"value": "1970-02-15, week of year 7"
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -105,7 +118,8 @@
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -116,7 +130,8 @@
|
||||
"value": "Year 1970, extended year 1970"
|
||||
},
|
||||
"platforms": [
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -128,7 +143,8 @@
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -140,7 +156,8 @@
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios"
|
||||
"ios",
|
||||
"web"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user