refactor(core): migrate format helpers to ts (#5273)

This commit is contained in:
Vladislav Shkodin 2021-04-18 15:02:24 +03:00 committed by GitHub
parent 0e629d342b
commit 667ee929d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 210 additions and 138 deletions

View File

@ -3,7 +3,15 @@ import { get } from 'lodash';
import yamlFormatter from './yaml';
import tomlFormatter from './toml';
import jsonFormatter from './json';
import { FrontmatterInfer, frontmatterJSON, frontmatterTOML, frontmatterYAML } from './frontmatter';
import {
FrontmatterInfer,
frontmatterJSON,
frontmatterTOML,
frontmatterYAML,
Delimiter,
} from './frontmatter';
import { Collection, EntryObject, Format } from '../types/redux';
import { EntryValue } from '../valueObjects/Entry';
export const frontmatterFormats = ['yaml-frontmatter', 'toml-frontmatter', 'json-frontmatter'];
@ -28,7 +36,7 @@ export const extensionFormatters = {
html: FrontmatterInfer,
};
function formatByName(name, customDelimiter) {
function formatByName(name: Format, customDelimiter?: Delimiter) {
return {
yml: yamlFormatter,
yaml: yamlFormatter,
@ -41,11 +49,17 @@ function formatByName(name, customDelimiter) {
}[name];
}
export function resolveFormat(collection, entry) {
function frontmatterDelimiterIsList(
frontmatterDelimiter?: Delimiter | List<string>,
): frontmatterDelimiter is List<string> {
return List.isList(frontmatterDelimiter);
}
export function resolveFormat(collection: Collection, entry: EntryObject | EntryValue) {
// Check for custom delimiter
const frontmatter_delimiter = collection.get('frontmatter_delimiter');
const customDelimiter = List.isList(frontmatter_delimiter)
? frontmatter_delimiter.toArray()
const customDelimiter = frontmatterDelimiterIsList(frontmatter_delimiter)
? (frontmatter_delimiter.toArray() as [string, string])
: frontmatter_delimiter;
// If the format is specified in the collection, use that format.
@ -58,8 +72,10 @@ export function resolveFormat(collection, entry) {
const filePath = entry && entry.path;
if (filePath) {
const fileExtension = filePath.split('.').pop();
if (fileExtension) {
return get(extensionFormatters, fileExtension);
}
}
// If creating a new file, and an `extension` is specified in the
// collection config, infer the format from that extension.

View File

@ -1,114 +0,0 @@
import matter from 'gray-matter';
import tomlFormatter from './toml';
import yamlFormatter from './yaml';
import jsonFormatter from './json';
const parsers = {
toml: {
parse: input => tomlFormatter.fromFile(input),
stringify: (metadata, { sortedKeys }) => tomlFormatter.toFile(metadata, sortedKeys),
},
json: {
parse: input => {
let JSONinput = input.trim();
// Fix JSON if leading and trailing brackets were trimmed.
if (JSONinput.substr(0, 1) !== '{') {
JSONinput = '{' + JSONinput + '}';
}
return jsonFormatter.fromFile(JSONinput);
},
stringify: (metadata, { sortedKeys }) => {
let JSONoutput = jsonFormatter.toFile(metadata, sortedKeys).trim();
// Trim leading and trailing brackets.
if (JSONoutput.substr(0, 1) === '{' && JSONoutput.substr(-1) === '}') {
JSONoutput = JSONoutput.substring(1, JSONoutput.length - 1);
}
return JSONoutput;
},
},
yaml: {
parse: input => yamlFormatter.fromFile(input),
stringify: (metadata, { sortedKeys, comments }) =>
yamlFormatter.toFile(metadata, sortedKeys, comments),
},
};
function inferFrontmatterFormat(str) {
const firstLine = str.substr(0, str.indexOf('\n')).trim();
if (firstLine.length > 3 && firstLine.substr(0, 3) === '---') {
// No need to infer, `gray-matter` will handle things like `---toml` for us.
return;
}
switch (firstLine) {
case '---':
return getFormatOpts('yaml');
case '+++':
return getFormatOpts('toml');
case '{':
return getFormatOpts('json');
default:
console.warn('Unrecognized front-matter format.');
}
}
export function getFormatOpts(format) {
return {
yaml: { language: 'yaml', delimiters: '---' },
toml: { language: 'toml', delimiters: '+++' },
json: { language: 'json', delimiters: ['{', '}'] },
}[format];
}
class FrontmatterFormatter {
constructor(format, customDelimiter) {
this.format = getFormatOpts(format);
this.customDelimiter = customDelimiter;
}
fromFile(content) {
const format = this.format || inferFrontmatterFormat(content);
if (this.customDelimiter) this.format.delimiters = this.customDelimiter;
const result = matter(content, { engines: parsers, ...format });
// in the absent of a body when serializing an entry we use an empty one
// when calling `toFile`, so we don't want to add it when parsing.
return {
...result.data,
...(result.content.trim() && { body: result.content }),
};
}
toFile(data, sortedKeys, comments = {}) {
const { body = '', ...meta } = data;
// Stringify to YAML if the format was not set
const format = this.format || getFormatOpts('yaml');
if (this.customDelimiter) this.format.delimiters = this.customDelimiter;
// gray-matter always adds a line break at the end which trips our
// change detection logic
// https://github.com/jonschlinkert/gray-matter/issues/96
const trimLastLineBreak = body.slice(-1) !== '\n' ? true : false;
// `sortedKeys` is not recognized by gray-matter, so it gets passed through to the parser
const file = matter.stringify(body, meta, {
engines: parsers,
sortedKeys,
comments,
...format,
});
return trimLastLineBreak && file.slice(-1) === '\n' ? file.substring(0, file.length - 1) : file;
}
}
export const FrontmatterInfer = new FrontmatterFormatter();
export function frontmatterYAML(customDelimiter) {
return new FrontmatterFormatter('yaml', customDelimiter);
}
export function frontmatterTOML(customDelimiter) {
return new FrontmatterFormatter('toml', customDelimiter);
}
export function frontmatterJSON(customDelimiter) {
return new FrontmatterFormatter('json', customDelimiter);
}

View File

@ -0,0 +1,146 @@
import matter from 'gray-matter';
import tomlFormatter from './toml';
import yamlFormatter from './yaml';
import jsonFormatter from './json';
enum Language {
YAML = 'yaml',
TOML = 'toml',
JSON = 'json',
}
export type Delimiter = string | [string, string];
type Format = { language: Language; delimiters: Delimiter };
const parsers = {
toml: {
parse: (input: string) => tomlFormatter.fromFile(input),
stringify: (metadata: object, opts?: { sortedKeys?: string[] }) => {
const { sortedKeys } = opts || {};
return tomlFormatter.toFile(metadata, sortedKeys);
},
},
json: {
parse: (input: string) => {
let JSONinput = input.trim();
// Fix JSON if leading and trailing brackets were trimmed.
if (JSONinput.substr(0, 1) !== '{') {
JSONinput = '{' + JSONinput + '}';
}
return jsonFormatter.fromFile(JSONinput);
},
stringify: (metadata: object) => {
let JSONoutput = jsonFormatter.toFile(metadata).trim();
// Trim leading and trailing brackets.
if (JSONoutput.substr(0, 1) === '{' && JSONoutput.substr(-1) === '}') {
JSONoutput = JSONoutput.substring(1, JSONoutput.length - 1);
}
return JSONoutput;
},
},
yaml: {
parse: (input: string) => yamlFormatter.fromFile(input),
stringify: (
metadata: object,
opts?: { sortedKeys?: string[]; comments?: Record<string, string> },
) => {
const { sortedKeys, comments } = opts || {};
return yamlFormatter.toFile(metadata, sortedKeys, comments);
},
},
};
function inferFrontmatterFormat(str: string) {
const firstLine = str.substr(0, str.indexOf('\n')).trim();
if (firstLine.length > 3 && firstLine.substr(0, 3) === '---') {
// No need to infer, `gray-matter` will handle things like `---toml` for us.
return;
}
switch (firstLine) {
case '---':
return getFormatOpts(Language.YAML);
case '+++':
return getFormatOpts(Language.TOML);
case '{':
return getFormatOpts(Language.JSON);
default:
console.warn('Unrecognized front-matter format.');
}
}
export function getFormatOpts(format?: Language, customDelimiter?: Delimiter) {
if (!format) {
return undefined;
}
const formats: { [key in Language]: Format } = {
yaml: { language: Language.YAML, delimiters: '---' },
toml: { language: Language.TOML, delimiters: '+++' },
json: { language: Language.JSON, delimiters: ['{', '}'] },
};
const { language, delimiters } = formats[format];
return {
language,
delimiters: customDelimiter || delimiters,
};
}
export class FrontmatterFormatter {
format?: Format;
constructor(format?: Language, customDelimiter?: Delimiter) {
this.format = getFormatOpts(format, customDelimiter);
}
fromFile(content: string) {
const format = this.format || inferFrontmatterFormat(content);
const result = matter(content, { engines: parsers, ...format });
// in the absent of a body when serializing an entry we use an empty one
// when calling `toFile`, so we don't want to add it when parsing.
return {
...result.data,
...(result.content.trim() && { body: result.content }),
};
}
toFile(
data: { body?: string } & Record<string, unknown>,
sortedKeys?: string[],
comments?: Record<string, string>,
) {
const { body = '', ...meta } = data;
// Stringify to YAML if the format was not set
const format = this.format || getFormatOpts(Language.YAML);
// gray-matter always adds a line break at the end which trips our
// change detection logic
// https://github.com/jonschlinkert/gray-matter/issues/96
const trimLastLineBreak = body.slice(-1) !== '\n';
const file = matter.stringify(body, meta, {
engines: parsers,
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore `sortedKeys` is not recognized by gray-matter, so it gets passed through to the parser
sortedKeys,
comments,
...format,
});
return trimLastLineBreak && file.slice(-1) === '\n' ? file.substring(0, file.length - 1) : file;
}
}
export const FrontmatterInfer = new FrontmatterFormatter();
export function frontmatterYAML(customDelimiter?: Delimiter) {
return new FrontmatterFormatter(Language.YAML, customDelimiter);
}
export function frontmatterTOML(customDelimiter?: Delimiter) {
return new FrontmatterFormatter(Language.TOML, customDelimiter);
}
export function frontmatterJSON(customDelimiter?: Delimiter) {
return new FrontmatterFormatter(Language.JSON, customDelimiter);
}

View File

@ -1,5 +1,9 @@
export function sortKeys(sortedKeys = [], selector = a => a) {
return (a, b) => {
export function sortKeys<Item extends unknown>(
sortedKeys: string[],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
selector: (a: Item) => string = (a: any) => a,
) {
return (a: Item, b: Item) => {
const idxA = sortedKeys.indexOf(selector(a));
const idxB = sortedKeys.indexOf(selector(b));
if (idxA === -1 || idxB === -1) return 0;

View File

@ -1,9 +1,9 @@
export default {
fromFile(content) {
fromFile(content: string) {
return JSON.parse(content);
},
toFile(data) {
toFile(data: object) {
return JSON.stringify(data, null, 2);
},
};

View File

@ -1,17 +1,19 @@
import toml from '@iarna/toml';
import tomlify from 'tomlify-j0.4';
import moment from 'moment';
import AssetProxy from 'ValueObjects/AssetProxy';
import AssetProxy from '../valueObjects/AssetProxy';
import { sortKeys } from './helpers';
function outputReplacer(key, value) {
function outputReplacer(_key: string, value: unknown) {
if (moment.isMoment(value)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
return value.format(value._f);
}
if (value instanceof AssetProxy) {
return `${value.path}`;
}
if (Number.isInteger(value)) {
if (typeof value === 'number' && Number.isInteger(value)) {
// Return the string representation of integers so tomlify won't render with tenths (".0")
return value.toString();
}
@ -20,11 +22,11 @@ function outputReplacer(key, value) {
}
export default {
fromFile(content) {
fromFile(content: string) {
return toml.parse(content);
},
toFile(data, sortedKeys = []) {
toFile(data: object, sortedKeys: string[] = []) {
return tomlify.toToml(data, { replace: outputReplacer, sort: sortKeys(sortedKeys) });
},
};

View File

@ -1,7 +1,8 @@
import yaml from 'yaml';
import { sortKeys } from './helpers';
import { YAMLMap, YAMLSeq, Pair, Node } from 'yaml/types';
function addComments(items, comments, prefix = '') {
function addComments(items: Array<Pair>, comments: Record<string, string>, prefix = '') {
items.forEach(item => {
if (item.key !== undefined) {
const itemKey = item.key.toString();
@ -18,7 +19,7 @@ function addComments(items, comments, prefix = '') {
}
const timestampTag = {
identify: value => value instanceof Date,
identify: (value: unknown) => value instanceof Date,
default: true,
tag: '!timestamp',
test: RegExp(
@ -29,20 +30,20 @@ const timestampTag = {
'Z' + // Z
'$',
),
resolve: str => new Date(str),
stringify: value => value.toISOString(),
};
resolve: (str: string) => new Date(str),
stringify: (value: Node) => (value as Date).toISOString(),
} as const;
export default {
fromFile(content) {
fromFile(content: string) {
if (content && content.trim().endsWith('---')) {
content = content.trim().slice(0, -3);
}
return yaml.parse(content, { customTags: [timestampTag] });
},
toFile(data, sortedKeys = [], comments = {}) {
const contents = yaml.createNode(data);
toFile(data: object, sortedKeys: string[] = [], comments: Record<string, string> = {}) {
const contents = yaml.createNode(data) as YAMLMap | YAMLSeq;
addComments(contents.items, comments);

View File

@ -9,6 +9,7 @@ import { Medias } from '../reducers/medias';
import { Deploys } from '../reducers/deploys';
import { Search } from '../reducers/search';
import { GlobalUI } from '../reducers/globalUI';
import { formatExtensions } from '../formats/formats';
export type CmsBackendType =
| 'azure'
@ -597,6 +598,8 @@ type i18n = StaticallyTypedRecord<{
default_locale: string;
}>;
export type Format = keyof typeof formatExtensions;
type CollectionObject = {
name: string;
folder?: string;
@ -611,7 +614,8 @@ type CollectionObject = {
filter?: FilterRule;
type: 'file_based_collection' | 'folder_based_collection';
extension?: string;
format?: string;
format?: Format;
frontmatter_delimiter?: List<string> | string | [string, string];
create?: boolean;
delete?: boolean;
identifier_field?: string;

View File

@ -0,0 +1,13 @@
declare module 'tomlify-j0.4' {
interface ToTomlOptions {
replace?(key: string, value: unknown): string | false;
sort?(a: string, b: string): number;
}
interface Tomlify {
toToml(data: object, options?: ToTomlOptions): string;
}
const tomlify: Tomlify;
export default tomlify;
}