This commit is contained in:
241
node_modules/decap-cms-lib-widgets/src/__tests__/stringTemplate.spec.js
generated
vendored
Normal file
241
node_modules/decap-cms-lib-widgets/src/__tests__/stringTemplate.spec.js
generated
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
import { fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
compileStringTemplate,
|
||||
expandPath,
|
||||
extractTemplateVars,
|
||||
keyToPathArray,
|
||||
parseDateFromEntry,
|
||||
} from '../stringTemplate';
|
||||
|
||||
describe('stringTemplate', () => {
|
||||
describe('keyToPathArray', () => {
|
||||
it('should return array of length 1 with simple path', () => {
|
||||
expect(keyToPathArray('category')).toEqual(['category']);
|
||||
});
|
||||
|
||||
it('should return path array for complex path', () => {
|
||||
expect(keyToPathArray('categories[0].title.subtitles[0].welcome[2]')).toEqual([
|
||||
'categories',
|
||||
'0',
|
||||
'title',
|
||||
'subtitles',
|
||||
'0',
|
||||
'welcome',
|
||||
'2',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseDateFromEntry', () => {
|
||||
it('should return date based on dateFieldName', () => {
|
||||
const date = new Date().toISOString();
|
||||
const dateFieldName = 'dateFieldName';
|
||||
const entry = fromJS({ data: { dateFieldName: date } });
|
||||
expect(parseDateFromEntry(entry, dateFieldName).toISOString()).toBe(date);
|
||||
});
|
||||
|
||||
it('should return undefined on empty dateFieldName', () => {
|
||||
const entry = fromJS({ data: {} });
|
||||
expect(parseDateFromEntry(entry, '')).toBeUndefined();
|
||||
expect(parseDateFromEntry(entry, null)).toBeUndefined();
|
||||
expect(parseDateFromEntry(entry, undefined)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined on invalid date', () => {
|
||||
const entry = fromJS({ data: { date: '' } });
|
||||
const dateFieldName = 'date';
|
||||
expect(parseDateFromEntry(entry, dateFieldName)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractTemplateVars', () => {
|
||||
it('should extract template variables', () => {
|
||||
expect(extractTemplateVars('{{slug}}-hello-{{date}}-world-{{fields.id}}')).toEqual([
|
||||
'slug',
|
||||
'date',
|
||||
'fields.id',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array on no matches', () => {
|
||||
expect(extractTemplateVars('hello-world')).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compileStringTemplate', () => {
|
||||
const date = new Date('2020-01-02T13:28:27.679Z');
|
||||
it('should compile year variable', () => {
|
||||
expect(compileStringTemplate('{{year}}', date)).toBe('2020');
|
||||
});
|
||||
|
||||
it('should compile month variable', () => {
|
||||
expect(compileStringTemplate('{{month}}', date)).toBe('01');
|
||||
});
|
||||
|
||||
it('should compile day variable', () => {
|
||||
expect(compileStringTemplate('{{day}}', date)).toBe('02');
|
||||
});
|
||||
|
||||
it('should compile hour variable', () => {
|
||||
expect(compileStringTemplate('{{hour}}', date)).toBe('13');
|
||||
});
|
||||
|
||||
it('should compile minute variable', () => {
|
||||
expect(compileStringTemplate('{{minute}}', date)).toBe('28');
|
||||
});
|
||||
|
||||
it('should compile second variable', () => {
|
||||
expect(compileStringTemplate('{{second}}', date)).toBe('27');
|
||||
});
|
||||
|
||||
it('should error on missing date', () => {
|
||||
expect(() => compileStringTemplate('{{year}}')).toThrowError();
|
||||
});
|
||||
|
||||
it('return compiled template', () => {
|
||||
expect(
|
||||
compileStringTemplate(
|
||||
'{{slug}}-{{year}}-{{fields.slug}}-{{title}}-{{date}}',
|
||||
date,
|
||||
'backendSlug',
|
||||
fromJS({ slug: 'entrySlug', title: 'title', date }),
|
||||
),
|
||||
).toBe('backendSlug-2020-entrySlug-title-' + date.toString());
|
||||
});
|
||||
|
||||
it('return apply processor to values', () => {
|
||||
expect(
|
||||
compileStringTemplate('{{slug}}', date, 'slug', fromJS({}), value => value.toUpperCase()),
|
||||
).toBe('SLUG');
|
||||
});
|
||||
|
||||
it('return apply filter to values', () => {
|
||||
expect(
|
||||
compileStringTemplate(
|
||||
'{{slug | upper}}-{{title | lower}}-{{year}}',
|
||||
date,
|
||||
'backendSlug',
|
||||
fromJS({ slug: 'entrySlug', title: 'Title', date }),
|
||||
),
|
||||
).toBe('BACKENDSLUG-title-2020');
|
||||
});
|
||||
|
||||
it('return apply filter to date field', () => {
|
||||
expect(
|
||||
compileStringTemplate(
|
||||
"{{slug | upper}}-{{title | lower}}-{{published | date('MM-DD')}}-{{year}}",
|
||||
date,
|
||||
'backendSlug',
|
||||
fromJS({ slug: 'entrySlug', title: 'Title', published: date, date }),
|
||||
),
|
||||
).toBe('BACKENDSLUG-title-01-02-2020');
|
||||
});
|
||||
|
||||
it('return apply filter for default value', () => {
|
||||
expect(
|
||||
compileStringTemplate(
|
||||
"{{slug | upper}}-{{title | default('none')}}-{{subtitle | default('none')}}",
|
||||
date,
|
||||
'backendSlug',
|
||||
fromJS({ slug: 'entrySlug', title: 'title', subtitle: null, published: date, date }),
|
||||
),
|
||||
).toBe('BACKENDSLUG-title-none');
|
||||
});
|
||||
|
||||
it('return apply filter for ternary', () => {
|
||||
expect(
|
||||
compileStringTemplate(
|
||||
"{{slug | upper}}-{{starred | ternary('star️','nostar')}}-{{done | ternary('done', 'open️')}}",
|
||||
date,
|
||||
'backendSlug',
|
||||
fromJS({ slug: 'entrySlug', starred: true, done: false }),
|
||||
),
|
||||
).toBe('BACKENDSLUG-star️-open️');
|
||||
});
|
||||
|
||||
it('return apply filter for truncate', () => {
|
||||
expect(
|
||||
compileStringTemplate(
|
||||
'{{slug | truncate(6)}}',
|
||||
date,
|
||||
'backendSlug',
|
||||
fromJS({ slug: 'entrySlug', starred: true, done: false }),
|
||||
),
|
||||
).toBe('backen...');
|
||||
});
|
||||
|
||||
it('return apply filter for truncate', () => {
|
||||
expect(
|
||||
compileStringTemplate(
|
||||
"{{slug | truncate(3,'***')}}",
|
||||
date,
|
||||
'backendSlug',
|
||||
fromJS({ slug: 'entrySlug', starred: true, done: false }),
|
||||
),
|
||||
).toBe('bac***');
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandPath', () => {
|
||||
it('should expand wildcard paths', () => {
|
||||
const data = {
|
||||
categories: [
|
||||
{
|
||||
name: 'category 1',
|
||||
},
|
||||
{
|
||||
name: 'category 2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(expandPath({ data, path: 'categories.*.name' })).toEqual([
|
||||
'categories.0.name',
|
||||
'categories.1.name',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle wildcard at the end of the path', () => {
|
||||
const data = {
|
||||
nested: {
|
||||
otherNested: {
|
||||
list: [
|
||||
{
|
||||
title: 'title 1',
|
||||
nestedList: [{ description: 'description 1' }, { description: 'description 2' }],
|
||||
},
|
||||
{
|
||||
title: 'title 2',
|
||||
nestedList: [{ description: 'description 2' }, { description: 'description 2' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(expandPath({ data, path: 'nested.otherNested.list.*.nestedList.*' })).toEqual([
|
||||
'nested.otherNested.list.0.nestedList.0',
|
||||
'nested.otherNested.list.0.nestedList.1',
|
||||
'nested.otherNested.list.1.nestedList.0',
|
||||
'nested.otherNested.list.1.nestedList.1',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle non wildcard index', () => {
|
||||
const data = {
|
||||
categories: [
|
||||
{
|
||||
name: 'category 1',
|
||||
},
|
||||
{
|
||||
name: 'category 2',
|
||||
},
|
||||
],
|
||||
};
|
||||
const path = 'categories.0.name';
|
||||
|
||||
expect(expandPath({ data, path })).toEqual(['categories.0.name']);
|
||||
});
|
||||
});
|
||||
});
|
||||
8
node_modules/decap-cms-lib-widgets/src/index.ts
generated
vendored
Normal file
8
node_modules/decap-cms-lib-widgets/src/index.ts
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as stringTemplate from './stringTemplate';
|
||||
import * as validations from './validations';
|
||||
|
||||
export const DecapCmsLibWidgets = {
|
||||
stringTemplate,
|
||||
validations,
|
||||
};
|
||||
export { stringTemplate, validations };
|
||||
248
node_modules/decap-cms-lib-widgets/src/stringTemplate.ts
generated
vendored
Normal file
248
node_modules/decap-cms-lib-widgets/src/stringTemplate.ts
generated
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
import { Map } from 'immutable';
|
||||
import { get, trimEnd, truncate } from 'lodash';
|
||||
import dayjs from 'dayjs';
|
||||
import { basename, dirname, extname } from 'path';
|
||||
|
||||
const filters = [
|
||||
{ pattern: /^upper$/, transform: (str: string) => str.toUpperCase() },
|
||||
{
|
||||
pattern: /^lower$/,
|
||||
transform: (str: string) => str.toLowerCase(),
|
||||
},
|
||||
{
|
||||
pattern: /^date\('(.+)'\)$/,
|
||||
transform: (str: string, match: RegExpMatchArray) => dayjs(str).format(match[1]),
|
||||
},
|
||||
{
|
||||
pattern: /^default\('(.+)'\)$/,
|
||||
transform: (str: string, match: RegExpMatchArray) => (str ? str : match[1]),
|
||||
},
|
||||
{
|
||||
pattern: /^ternary\('(.*)',\s*'(.*)'\)$/,
|
||||
transform: (str: string, match: RegExpMatchArray) => (str ? match[1] : match[2]),
|
||||
},
|
||||
{
|
||||
pattern: /^truncate\(([0-9]+)(?:(?:,\s*['"])([^'"]*)(?:['"]))?\)$/,
|
||||
transform: (str: string, match: RegExpMatchArray) => {
|
||||
const omission = match[2] || '...';
|
||||
const length = parseInt(match[1]) + omission.length;
|
||||
|
||||
return truncate(str, {
|
||||
length,
|
||||
omission,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const FIELD_PREFIX = 'fields.';
|
||||
const templateContentPattern = '([^}{|]+)';
|
||||
const filterPattern = '( \\| ([^}{]+))?';
|
||||
const templateVariablePattern = `{{${templateContentPattern}${filterPattern}}}`;
|
||||
|
||||
// prepends a Zero if the date has only 1 digit
|
||||
function formatDate(date: number) {
|
||||
return `0${date}`.slice(-2);
|
||||
}
|
||||
|
||||
export const dateParsers: Record<string, (date: Date) => string> = {
|
||||
year: (date: Date) => `${date.getUTCFullYear()}`,
|
||||
month: (date: Date) => formatDate(date.getUTCMonth() + 1),
|
||||
day: (date: Date) => formatDate(date.getUTCDate()),
|
||||
hour: (date: Date) => formatDate(date.getUTCHours()),
|
||||
minute: (date: Date) => formatDate(date.getUTCMinutes()),
|
||||
second: (date: Date) => formatDate(date.getUTCSeconds()),
|
||||
};
|
||||
|
||||
export function parseDateFromEntry(entry: Map<string, unknown>, dateFieldName?: string | null) {
|
||||
if (!dateFieldName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dateValue = entry.getIn(['data', dateFieldName]);
|
||||
const dateDayjs = dateValue && dayjs(dateValue);
|
||||
if (dateDayjs && dateDayjs.isValid()) {
|
||||
return dateDayjs.toDate();
|
||||
}
|
||||
}
|
||||
|
||||
export const SLUG_MISSING_REQUIRED_DATE = 'SLUG_MISSING_REQUIRED_DATE';
|
||||
|
||||
export function keyToPathArray(key?: string) {
|
||||
if (!key) {
|
||||
return [];
|
||||
}
|
||||
const parts = [];
|
||||
const separator = '';
|
||||
const chars = key.split(separator);
|
||||
|
||||
let currentChar;
|
||||
let currentStr = [];
|
||||
while ((currentChar = chars.shift())) {
|
||||
if (['[', ']', '.'].includes(currentChar)) {
|
||||
if (currentStr.length > 0) {
|
||||
parts.push(currentStr.join(separator));
|
||||
}
|
||||
currentStr = [];
|
||||
} else {
|
||||
currentStr.push(currentChar);
|
||||
}
|
||||
}
|
||||
if (currentStr.length > 0) {
|
||||
parts.push(currentStr.join(separator));
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function expandPath({
|
||||
data,
|
||||
path,
|
||||
paths = [],
|
||||
}: {
|
||||
data: Record<string, unknown>;
|
||||
path: string;
|
||||
paths?: string[];
|
||||
}) {
|
||||
if (path.endsWith('.*')) {
|
||||
path = path + '.';
|
||||
}
|
||||
|
||||
const sep = '.*.';
|
||||
const parts = path.split(sep);
|
||||
if (parts.length === 1) {
|
||||
paths.push(path);
|
||||
} else {
|
||||
const partialPath = parts[0];
|
||||
const value = get(data, partialPath);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((_, index) => {
|
||||
expandPath({
|
||||
data,
|
||||
path: trimEnd(`${partialPath}.${index}.${parts.slice(1).join(sep)}`, '.'),
|
||||
paths,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
// Allow `fields.` prefix in placeholder to override built in replacements
|
||||
// like "slug" and "year" with values from fields of the same name.
|
||||
function getExplicitFieldReplacement(key: string, data: Map<string, unknown>) {
|
||||
if (!key.startsWith(FIELD_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
const fieldName = key.slice(FIELD_PREFIX.length);
|
||||
const value = data.getIn(keyToPathArray(fieldName));
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getFilterFunction(filterStr: string) {
|
||||
if (filterStr) {
|
||||
let match: RegExpMatchArray | null = null;
|
||||
const filter = filters.find(filter => {
|
||||
match = filterStr.match(filter.pattern);
|
||||
return !!match;
|
||||
});
|
||||
|
||||
if (filter) {
|
||||
return (str: string) => filter.transform(str, match as RegExpMatchArray);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function compileStringTemplate(
|
||||
template: string,
|
||||
date: Date | undefined | null,
|
||||
identifier = '',
|
||||
data = Map<string, unknown>(),
|
||||
processor?: (value: string) => string,
|
||||
) {
|
||||
let missingRequiredDate;
|
||||
|
||||
// Turn off date processing (support for replacements like `{{year}}`), by passing in
|
||||
// `null` as the date arg.
|
||||
const useDate = date !== null;
|
||||
|
||||
const compiledString = template.replace(
|
||||
RegExp(templateVariablePattern, 'g'),
|
||||
(_full, key: string, _part, filter: string) => {
|
||||
let replacement;
|
||||
const explicitFieldReplacement = getExplicitFieldReplacement(key, data);
|
||||
|
||||
if (explicitFieldReplacement) {
|
||||
replacement = explicitFieldReplacement;
|
||||
} else if (dateParsers[key] && !date) {
|
||||
missingRequiredDate = true;
|
||||
return '';
|
||||
} else if (dateParsers[key]) {
|
||||
replacement = dateParsers[key](date as Date);
|
||||
} else if (key === 'slug') {
|
||||
replacement = identifier;
|
||||
} else {
|
||||
replacement = data.getIn(keyToPathArray(key), '') as string;
|
||||
}
|
||||
|
||||
if (processor) {
|
||||
return processor(replacement);
|
||||
} else {
|
||||
const filterFunction = getFilterFunction(filter);
|
||||
if (filterFunction) {
|
||||
replacement = filterFunction(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
return replacement;
|
||||
},
|
||||
);
|
||||
|
||||
if (useDate && missingRequiredDate) {
|
||||
const err = new Error();
|
||||
err.name = SLUG_MISSING_REQUIRED_DATE;
|
||||
throw err;
|
||||
} else {
|
||||
return compiledString;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractTemplateVars(template: string) {
|
||||
const regexp = RegExp(templateVariablePattern, 'g');
|
||||
const contentRegexp = RegExp(templateContentPattern, 'g');
|
||||
const matches = template.match(regexp) || [];
|
||||
return matches.map(elem => {
|
||||
const match = elem.match(contentRegexp);
|
||||
return match ? match[0] : '';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends `dirname`, `filename` and `extension` to the provided `fields` map.
|
||||
* @param entryPath
|
||||
* @param fields
|
||||
* @param folder - optionally include a folder that the dirname will be relative to.
|
||||
* eg: `addFileTemplateFields('foo/bar/baz.ext', fields, 'foo')`
|
||||
* will result in: `{ dirname: 'bar', filename: 'baz', extension: 'ext' }`
|
||||
*/
|
||||
export function addFileTemplateFields(entryPath: string, fields: Map<string, string>, folder = '') {
|
||||
if (!entryPath) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
const extension = extname(entryPath);
|
||||
const filename = basename(entryPath, extension);
|
||||
const dirnameExcludingFolder = dirname(entryPath).replace(new RegExp(`^(/?)${folder}/?`), '$1');
|
||||
fields = fields.withMutations(map => {
|
||||
map.set('dirname', dirnameExcludingFolder);
|
||||
map.set('filename', filename);
|
||||
map.set('extension', extension === '' ? extension : extension.slice(1));
|
||||
});
|
||||
|
||||
return fields;
|
||||
}
|
||||
31
node_modules/decap-cms-lib-widgets/src/validations.ts
generated
vendored
Normal file
31
node_modules/decap-cms-lib-widgets/src/validations.ts
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
import type { List } from 'immutable';
|
||||
|
||||
export function validateMinMax(
|
||||
t: (key: string, options: unknown) => string,
|
||||
fieldLabel: string,
|
||||
value?: List<unknown>,
|
||||
min?: number,
|
||||
max?: number,
|
||||
) {
|
||||
function minMaxError(messageKey: string) {
|
||||
return {
|
||||
type: 'RANGE',
|
||||
message: t(`editor.editorControlPane.widget.${messageKey}`, {
|
||||
fieldLabel,
|
||||
minCount: min,
|
||||
maxCount: max,
|
||||
count: min,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if ([min, max, value?.size].every(isNumber) && (value!.size < min! || value!.size > max!)) {
|
||||
return minMaxError(min === max ? 'rangeCountExact' : 'rangeCount');
|
||||
} else if (isNumber(min) && min > 0 && value?.size && value.size < min) {
|
||||
return minMaxError('rangeMin');
|
||||
} else if (isNumber(max) && value?.size && value.size > max) {
|
||||
return minMaxError('rangeMax');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user