refactor(core): migrate format helpers to ts (#5273)
This commit is contained in:
parent
0e629d342b
commit
667ee929d3
@ -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.
|
@ -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);
|
||||
}
|
146
packages/netlify-cms-core/src/formats/frontmatter.ts
Normal file
146
packages/netlify-cms-core/src/formats/frontmatter.ts
Normal 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);
|
||||
}
|
@ -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;
|
@ -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);
|
||||
},
|
||||
};
|
@ -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) });
|
||||
},
|
||||
};
|
@ -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);
|
||||
|
@ -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;
|
||||
|
13
packages/netlify-cms-core/src/types/tomlify-j0.4.d.ts
vendored
Normal file
13
packages/netlify-cms-core/src/types/tomlify-j0.4.d.ts
vendored
Normal 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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user