Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support writing frontmatter in multiple formats. #933

Merged
merged 7 commits into from
Jan 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/backends/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,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 });
}
Expand Down
106 changes: 96 additions & 10 deletions src/formats/__tests__/frontmatter.spec.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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(
[
'---',
Expand All @@ -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(
[
'---',
Expand All @@ -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')
);
});
});
19 changes: 14 additions & 5 deletions src/formats/formats.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
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',
'yaml',
'toml',
'json',
'frontmatter',
'json-frontmatter',
'toml-frontmatter',
'yaml-frontmatter',
];

export const formatToExtension = format => ({
Expand All @@ -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) {
Expand All @@ -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];
}

Expand All @@ -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];
}

Expand Down
69 changes: 50 additions & 19 deletions src/formats/frontmatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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');
2 changes: 1 addition & 1 deletion src/formats/json.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ export default {
},

toFile(data) {
return JSON.stringify(data);
return JSON.stringify(data, null, 2);
}
}
7 changes: 5 additions & 2 deletions website/site/content/docs/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down