Support writing frontmatter in multiple formats (#933)

* Format JSON files.

Currently we store JSON as a single line in files. We should prettify it
like we do the other formats.

* Add output parsers to the frontmatter list.

* Cleanup frontmatter format parser options.

* Support writing frontmatter in TOML and JSON.

Right now we can read TOML or JSON frontmatter by inferring,
but we can only write frontmatter in YAML. This change allows the
frontmatter format to be explicitly set for reading and writing.

* Fix frontmatter formatter.

* Update Frontmatter formatter tests.

* Update frontmatter format docs.
This commit is contained in:
Caleb 2018-01-29 15:35:36 -07:00 committed by Shawn Erquhart
parent 61e0b23194
commit 756d562c66
5 changed files with 166 additions and 37 deletions

View File

@ -173,7 +173,7 @@ class Backend {
return (entry) => {
const format = resolveFormat(collectionOrEntity, entry);
if (entry && entry.raw !== undefined) {
const data = (format && attempt(format.fromFile.bind(null, entry.raw))) || {};
const data = (format && attempt(format.fromFile.bind(format, entry.raw))) || {};
if (isError(data)) console.error(data);
return Object.assign(entry, { data: isError(data) ? {} : data });
}

View File

@ -1,11 +1,11 @@
import FrontmatterFormatter from '../frontmatter';
import { FrontmatterInfer, FrontmatterJSON, FrontmatterTOML, FrontmatterYAML } from '../frontmatter';
jest.mock("../../valueObjects/AssetProxy.js");
describe('Frontmatter', () => {
it('should parse YAML with --- delimiters', () => {
expect(
FrontmatterFormatter.fromFile('---\ntitle: YAML\ndescription: Something longer\n---\nContent')
FrontmatterInfer.fromFile('---\ntitle: YAML\ndescription: Something longer\n---\nContent')
).toEqual(
{
title: 'YAML',
@ -15,9 +15,21 @@ describe('Frontmatter', () => {
);
});
it('should parse YAML with --- delimiters when it is explicitly set as the format', () => {
expect(
FrontmatterYAML.fromFile('---\ntitle: YAML\ndescription: Something longer\n---\nContent')
).toEqual(
{
title: 'YAML',
description: 'Something longer',
body: 'Content',
}
);
});
it('should parse YAML with ---yaml delimiters', () => {
expect(
FrontmatterFormatter.fromFile('---yaml\ntitle: YAML\ndescription: Something longer\n---\nContent')
FrontmatterInfer.fromFile('---yaml\ntitle: YAML\ndescription: Something longer\n---\nContent')
).toEqual(
{
title: 'YAML',
@ -29,7 +41,7 @@ describe('Frontmatter', () => {
it('should overwrite any body param in the front matter', () => {
expect(
FrontmatterFormatter.fromFile('---\ntitle: The Title\nbody: Something longer\n---\nContent')
FrontmatterInfer.fromFile('---\ntitle: The Title\nbody: Something longer\n---\nContent')
).toEqual(
{
title: 'The Title',
@ -40,7 +52,7 @@ describe('Frontmatter', () => {
it('should parse TOML with +++ delimiters', () => {
expect(
FrontmatterFormatter.fromFile('+++\ntitle = "TOML"\ndescription = "Front matter"\n+++\nContent')
FrontmatterInfer.fromFile('+++\ntitle = "TOML"\ndescription = "Front matter"\n+++\nContent')
).toEqual(
{
title: 'TOML',
@ -50,9 +62,21 @@ describe('Frontmatter', () => {
);
});
it('should parse TOML with +++ delimiters when it is explicitly set as the format', () => {
expect(
FrontmatterTOML.fromFile('+++\ntitle = "TOML"\ndescription = "Front matter"\n+++\nContent')
).toEqual(
{
title: 'TOML',
description: 'Front matter',
body: 'Content',
}
);
});
it('should parse TOML with ---toml delimiters', () => {
expect(
FrontmatterFormatter.fromFile('---toml\ntitle = "TOML"\ndescription = "Something longer"\n---\nContent')
FrontmatterInfer.fromFile('---toml\ntitle = "TOML"\ndescription = "Something longer"\n---\nContent')
).toEqual(
{
title: 'TOML',
@ -64,7 +88,7 @@ describe('Frontmatter', () => {
it('should parse JSON with { } delimiters', () => {
expect(
FrontmatterFormatter.fromFile('{\n"title": "The Title",\n"description": "Something longer"\n}\nContent')
FrontmatterInfer.fromFile('{\n"title": "The Title",\n"description": "Something longer"\n}\nContent')
).toEqual(
{
title: 'The Title',
@ -74,9 +98,21 @@ describe('Frontmatter', () => {
);
});
it('should parse JSON with { } delimiters when it is explicitly set as the format', () => {
expect(
FrontmatterJSON.fromFile('{\n"title": "The Title",\n"description": "Something longer"\n}\nContent')
).toEqual(
{
title: 'The Title',
description: 'Something longer',
body: 'Content',
}
);
});
it('should parse JSON with ---json delimiters', () => {
expect(
FrontmatterFormatter.fromFile('---json\n{\n"title": "The Title",\n"description": "Something longer"\n}\n---\nContent')
FrontmatterInfer.fromFile('---json\n{\n"title": "The Title",\n"description": "Something longer"\n}\n---\nContent')
).toEqual(
{
title: 'The Title',
@ -88,7 +124,7 @@ describe('Frontmatter', () => {
it('should stringify YAML with --- delimiters', () => {
expect(
FrontmatterFormatter.toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'yaml'], title: 'YAML' })
FrontmatterInfer.toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'yaml'], title: 'YAML' })
).toEqual(
[
'---',
@ -105,7 +141,7 @@ describe('Frontmatter', () => {
it('should stringify YAML with missing body', () => {
expect(
FrontmatterFormatter.toFile({ tags: ['front matter', 'yaml'], title: 'YAML' })
FrontmatterInfer.toFile({ tags: ['front matter', 'yaml'], title: 'YAML' })
).toEqual(
[
'---',
@ -119,4 +155,54 @@ describe('Frontmatter', () => {
].join('\n')
);
});
it('should stringify YAML with --- delimiters when it is explicitly set as the format', () => {
expect(
FrontmatterYAML.toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'yaml'], title: 'YAML' })
).toEqual(
[
'---',
'tags:',
' - front matter',
' - yaml',
'title: YAML',
'---',
'Some content',
'On another line\n',
].join('\n')
);
});
it('should stringify TOML with +++ delimiters when it is explicitly set as the format', () => {
expect(
FrontmatterTOML.toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'toml'], title: 'TOML' })
).toEqual(
[
'+++',
'tags = ["front matter", "toml"]',
'title = "TOML"',
'+++',
'Some content',
'On another line\n',
].join('\n')
);
});
it('should stringify JSON with { } delimiters when it is explicitly set as the format', () => {
expect(
FrontmatterJSON.toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'json'], title: 'JSON' })
).toEqual(
[
'{',
'"tags": [',
' "front matter",',
' "json"',
' ],',
' "title": "JSON"',
'}',
'Some content',
'On another line\n',
].join('\n')
);
});
});

View File

@ -1,7 +1,7 @@
import yamlFormatter from './yaml';
import tomlFormatter from './toml';
import jsonFormatter from './json';
import FrontmatterFormatter from './frontmatter';
import { FrontmatterInfer, FrontmatterJSON, FrontmatterTOML, FrontmatterYAML } from './frontmatter';
export const supportedFormats = [
'yml',
@ -9,6 +9,9 @@ export const supportedFormats = [
'toml',
'json',
'frontmatter',
'json-frontmatter',
'toml-frontmatter',
'yaml-frontmatter',
];
export const formatToExtension = format => ({
@ -17,6 +20,9 @@ export const formatToExtension = format => ({
toml: 'toml',
json: 'json',
frontmatter: 'md',
'json-frontmatter': 'md',
'toml-frontmatter': 'md',
'yaml-frontmatter': 'md',
}[format]);
export function formatByExtension(extension) {
@ -25,9 +31,9 @@ export function formatByExtension(extension) {
yaml: yamlFormatter,
toml: tomlFormatter,
json: jsonFormatter,
md: FrontmatterFormatter,
markdown: FrontmatterFormatter,
html: FrontmatterFormatter,
md: FrontmatterInfer,
markdown: FrontmatterInfer,
html: FrontmatterInfer,
}[extension];
}
@ -37,7 +43,10 @@ function formatByName(name) {
yaml: yamlFormatter,
toml: tomlFormatter,
json: jsonFormatter,
frontmatter: FrontmatterFormatter,
frontmatter: FrontmatterInfer,
'json-frontmatter': FrontmatterJSON,
'toml-frontmatter': FrontmatterTOML,
'yaml-frontmatter': FrontmatterYAML,
}[name];
}

View File

@ -4,17 +4,30 @@ import yamlFormatter from './yaml';
import jsonFormatter from './json';
const parsers = {
toml: input => tomlFormatter.fromFile(input),
json: input => {
let JSONinput = input.trim();
// Fix JSON if leading and trailing brackets were trimmed.
if (JSONinput.substr(0, 1) !== '{') {
JSONinput = '{' + JSONinput;
}
if (JSONinput.substr(-1) !== '}') {
JSONinput = JSONinput + '}';
}
return jsonFormatter.fromFile(JSONinput);
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;
}
if (JSONinput.substr(-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),
@ -30,30 +43,48 @@ function inferFrontmatterFormat(str) {
}
switch (firstLine) {
case "---":
return { language: "yaml", delimiters: "---" };
return getFormatOpts('yaml');
case "+++":
return { language: "toml", delimiters: "+++" };
return getFormatOpts('toml');
case "{":
return { language: "json", delimiters: ["{", "}"] };
return getFormatOpts('json');
default:
throw "Unrecognized front-matter format.";
}
}
export default {
export const getFormatOpts = format => ({
yaml: { language: "yaml", delimiters: "---" },
toml: { language: "toml", delimiters: "+++" },
json: { language: "json", delimiters: ["{", "}"] },
}[format]);
class FrontmatterFormatter {
constructor(format) {
this.format = getFormatOpts(format);
}
fromFile(content) {
const result = matter(content, { engines: parsers, ...inferFrontmatterFormat(content) });
const format = this.format || inferFrontmatterFormat(content);
const result = matter(content, { engines: parsers, ...format });
return {
...result.data,
body: result.content,
};
},
}
toFile(data, sortedKeys) {
const { body = '', ...meta } = data;
// always stringify to YAML
// Stringify to YAML if the format was not set
const format = this.format || getFormatOpts('yaml');
// `sortedKeys` is not recognized by gray-matter, so it gets passed through to the parser
return matter.stringify(body, meta, { engines: parsers, language: "yaml", delimiters: "---", sortedKeys });
return matter.stringify(body, meta, { engines: parsers, sortedKeys, ...format });
}
}
export const FrontmatterInfer = new FrontmatterFormatter();
export const FrontmatterYAML = new FrontmatterFormatter('yaml');
export const FrontmatterTOML = new FrontmatterFormatter('toml');
export const FrontmatterJSON = new FrontmatterFormatter('json');

View File

@ -95,9 +95,12 @@ You may also specify a custom `extension` not included in the list above, as lon
- `yml` or `yaml`: parses and saves files as YAML-formatted data files; saves with `yml` extension by default
- `toml`: parses and saves files as TOML-formatted data files; saves with `toml` extension by default
- `json`: parses and saves files as JSON-formatted data files; saves with `json` extension by default
- `frontmatter`: parses files and saves files with data frontmatter followed by an unparsed body text (edited using a `body` field); saves with `md` extension by default; default for collections that can't be inferred
- `frontmatter`: parses files and saves files with data frontmatter followed by an unparsed body text (edited using a `body` field); saves with `md` extension by default; default for collections that can't be inferred. Collections with `frontmatter` format (either inferred or explicitly set) can parse files with frontmatter in YAML, TOML, or JSON format. However, they will be saved with YAML frontmatter.
- `yaml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as YAML, followed by unparsed body text
- `toml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as TOML, followed by unparsed body text
- `json-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved as JSON, followed by unparsed body text
Collections with `frontmatter` format (either inferred or explicitly set) can parse files with frontmatter in YAML, TOML, or JSON format. On saving, however, they will currently be saved with YAML frontmatter. (Follow [Issue #563](https://github.com/netlify/netlify-cms/issues/563)) to see when this changes.)
The explicit `yaml-frontmatter`, `toml-frontmatter`, and `json-frontmatter` formats above do not currently support custom delimiters. We use `---` for YAML, `+++` for TOML, and `{` `}` for JSON. If a file has frontmatter inside other delimiters it will be included as part of the body text.
### `slug`