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

feat: register custom formats take 2 #6205

Merged
merged 35 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fbe967a
feat: register custom formats
jlabedo Sep 24, 2021
f7952f2
fix: register custom formats validation
jlabedo Sep 29, 2021
988a06d
fix: change 'format' field validation to string instead of enum
jlabedo Sep 30, 2021
1dc43db
test: custom formats and register function
mmkal Feb 12, 2022
6d29b23
docs: explain usage and note manual initialization requirement
mmkal Feb 12, 2022
faea3ee
Merge branch 'master' into mk/custom-formatters
mmkal Feb 12, 2022
3c63e9b
fix: remove unused imports
mmkal Feb 12, 2022
38d5b0a
Merge branch 'mk/custom-formatters' of https://github.com/mmkal/netli…
mmkal Feb 12, 2022
8a9fd30
Merge branch 'master' into mk/custom-formatters
erezrokah Feb 17, 2022
9fcbd10
Merge branch 'master' into mk/custom-formatters
erezrokah Feb 18, 2022
21c7c3a
Merge branch 'master' into mk/custom-formatters
erezrokah Feb 18, 2022
fe3e774
Merge branch 'master' into mk/custom-formatters
erezrokah Feb 21, 2022
0784a26
Merge branch 'master' into mk/custom-formatters
mmkal Feb 26, 2022
50ea388
Merge branch 'master' into mk/custom-formatters
erezrokah Feb 28, 2022
82c7bf4
Merge branch 'master' into mk/custom-formatters
mmkal Mar 15, 2022
2c1b4f5
Merge branch 'master' into mk/custom-formatters
erezrokah Apr 13, 2022
9acee6a
use default extension
mmkal Apr 23, 2022
0ad477a
remove manual init note
mmkal Apr 23, 2022
72a2cb8
Merge branch 'master' into mk/custom-formatters
mmkal Apr 23, 2022
7cc182d
Merge branch 'master' into mk/custom-formatters
mmkal Jun 7, 2022
2f95d52
PR comments
mmkal Jun 30, 2022
ff3d74e
Merge branch 'mk/custom-formatters' of https://github.com/mmkal/netli…
mmkal Jun 30, 2022
aea2bed
fix: prettier
mmkal Jul 17, 2022
a0c8efe
revert unnecessary changes?
mmkal Aug 16, 2022
b7886b8
chore: more revert?
mmkal Aug 16, 2022
8e8a248
chore: newline
mmkal Aug 16, 2022
178c4f1
Merge branch 'master' into mk/custom-formatters
mmkal Apr 27, 2023
cdbcb99
Merge branch 'master' into mk/custom-formatters
mmkal May 22, 2023
517085e
Merge branch 'master' into mk/custom-formatters
mmkal May 29, 2023
aed01d0
Merge branch 'master' into mk/custom-formatters
mmkal Jun 13, 2023
d4ec555
Merge branch 'master' into mk/custom-formatters
mmkal Aug 2, 2023
8c1b5b6
Merge branch 'master' into mk/custom-formatters
mmkal Aug 16, 2023
f111d8c
Merge branch 'master' into mk/custom-formatters
mmkal Aug 23, 2023
08e4b68
Merge branch 'mk/custom-formatters' of https://github.com/mmkal/netli…
mmkal Aug 23, 2023
bd162e4
chore: update import
mmkal Aug 23, 2023
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
21 changes: 21 additions & 0 deletions dev-test/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ publish_mode: editorial_workflow
media_folder: assets/uploads

collections: # A list of collections the CMS should be able to edit
- name: 'translations'
label: 'Translations'
folder: '_translations'
i18n:
structure: 'multiple_folders'
format: custom
identifier_field: namespace
fields:
- label: Namespace
name: namespace
widget: string
i18n: false
- label: 'Items'
label_singular: 'Item'
name: 'list'
widget: list
fields:
- name: key
widget: string
- name: value
widget: string
- name: 'posts' # Used in routes, ie.: /admin/collections/:slug/edit
label: 'Posts' # Used in the UI
label_singular: 'Post' # Used in the UI, ie: "New Post"
Expand Down
32 changes: 32 additions & 0 deletions dev-test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
<title>Netlify CMS Development Test</title>
<script>
window.repoFiles = {
_translations: {
"common.json": {
content: '{"__ns": "common", "key1": "hello"}'
}
},
_posts: {
"2015-02-14-this-is-a-post.md": {
content: "---\ntitle: This is a YAML front matter post\nimage: /nf-logo.png\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n"
Expand Down Expand Up @@ -111,6 +116,9 @@
</script>
</head>
<body>
<script>
globalThis.CMS_MANUAL_INIT = true
</script>

<script src="dist/netlify-cms.js"></script>
<script>
Expand Down Expand Up @@ -237,6 +245,30 @@
);
}
});

const PreWidget = createClass({
mmkal marked this conversation as resolved.
Show resolved Hide resolved
render: function() {
const { value, fieldsMetaData } = this.props;
return value ? h('pre', { style: style },
value.toJS()
) : null;
}
});

CMS.registerCustomFormat('custom', 'json', {
fromFile(content) {
const {__ns, ...others} = JSON.parse(content);
const list = Object.entries(others).map(([key, value]) => ({key, value}))
return {namespace: __ns, list};
},

toFile(data) {
const keys = data.list.reduce((acc, item) => ({...acc, [item.key]: item.value}), {})
const raw = {__ns: data.namespace, ...keys};
return JSON.stringify(raw, null, 2);
},
});
CMS.init()
</script>
</body>
</html>
19 changes: 10 additions & 9 deletions packages/netlify-cms-core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,7 @@ declare module 'netlify-cms-core' {
value: any;
}

export type CmsCollectionFormatType =
| 'yml'
| 'yaml'
| 'toml'
| 'json'
| 'frontmatter'
| 'yaml-frontmatter'
| 'toml-frontmatter'
| 'json-frontmatter';
export type CmsCollectionFormatType = string;

export type CmsAuthScope = 'repo' | 'public_repo';

Expand Down Expand Up @@ -500,6 +492,11 @@ declare module 'netlify-cms-core' {

export type CmsLocalePhrases = any; // TODO: type properly

export type Formatter = {
fromFile(content: string): unknown;
toFile(data: object, sortedKeys?: string[], comments?: Record<string, string>): string;
};

export interface CmsRegistry {
backends: {
[name: string]: CmsRegistryBackend;
Expand All @@ -519,6 +516,9 @@ declare module 'netlify-cms-core' {
locales: {
[name: string]: CmsLocalePhrases;
};
formats: {
[name: string]: Formatter;
};
}

type GetAssetFunction = (asset: string) => {
Expand Down Expand Up @@ -578,6 +578,7 @@ declare module 'netlify-cms-core' {
serializer: CmsWidgetValueSerializer,
) => void;
resolveWidget: (name: string) => CmsWidget | undefined;
registerCustomFormat: (name: string, extension: string, formatter: Formatter) => void;
}

export const NetlifyCmsCore: CMS;
Expand Down
4 changes: 2 additions & 2 deletions packages/netlify-cms-core/src/constants/configSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import ajvErrors from 'ajv-errors';
import uuid from 'uuid/v4';

import { formatExtensions, frontmatterFormats, extensionFormatters } from '../formats/formats';
import { frontmatterFormats, extensionFormatters } from '../formats/formats';
import { getWidgets } from '../lib/registry';
import { I18N_STRUCTURE, I18N_FIELD } from '../lib/i18n';

Expand Down Expand Up @@ -231,7 +231,7 @@ function getConfigSchema() {
preview: { type: 'boolean' },
},
},
format: { type: 'string', enum: Object.keys(formatExtensions) },
format: { type: 'string' },
extension: { type: 'string' },
frontmatter_delimiter: {
type: ['string', 'array'],
Expand Down
87 changes: 87 additions & 0 deletions packages/netlify-cms-core/src/formats/__tests__/formats.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Map } from 'immutable';

import { extensionFormatters, resolveFormat } from '../formats';
import { registerCustomFormat } from '../../lib/registry';

describe('custom formats', () => {
const testEntry = {
collection: 'testCollection',
data: { x: 1 },
isModification: false,
label: 'testLabel',
mediaFiles: [],
meta: {},
newRecord: true,
partial: false,
path: 'testPath1',
raw: 'testRaw',
slug: 'testSlug',
author: 'testAuthor',
updatedOn: 'testUpdatedOn',
};
it('resolves builtint formats', () => {
const collection = Map({
name: 'posts',
});
expect(resolveFormat(collection, { ...testEntry, path: 'test.yml' })).toEqual(
extensionFormatters.yml,
);
expect(resolveFormat(collection, { ...testEntry, path: 'test.yaml' })).toEqual(
extensionFormatters.yml,
);
expect(resolveFormat(collection, { ...testEntry, path: 'test.toml' })).toEqual(
extensionFormatters.toml,
);
expect(resolveFormat(collection, { ...testEntry, path: 'test.json' })).toEqual(
extensionFormatters.json,
);
expect(resolveFormat(collection, { ...testEntry, path: 'test.md' })).toEqual(
extensionFormatters.md,
);
expect(resolveFormat(collection, { ...testEntry, path: 'test.markdown' })).toEqual(
extensionFormatters.markdown,
);
expect(resolveFormat(collection, { ...testEntry, path: 'test.html' })).toEqual(
extensionFormatters.html,
);
});

it('resolves custom format', () => {
registerCustomFormat('txt-querystring', 'txt', {
fromFile: file => Object.fromEntries(new URLSearchParams(file)),
toFile: value => new URLSearchParams(value).toString(),
});

const collection = Map({
name: 'posts',
format: 'txt-querystring',
});

const formatter = resolveFormat(collection, { ...testEntry, path: 'test.txt' });

expect(formatter.toFile({ foo: 'bar' })).toEqual('foo=bar');
expect(formatter.fromFile('foo=bar')).toEqual({ foo: 'bar' });
});

it('can override existing formatters', () => {
// simplified version of a more realistic use case: using a different yaml library like js-yaml
// to make netlify-cms play nice with other tools that edit content and spit out yaml
registerCustomFormat('bad-yaml', 'yml', {
fromFile: file => Object.fromEntries(file.split('\n').map(line => line.split(': '))),
toFile: value =>
Object.entries(value)
.map(([k, v]) => `${k}: ${v}`)
.join('\n'),
});

const collection = Map({
name: 'posts',
format: 'bad-yaml',
});

const formatter = resolveFormat(collection, { ...testEntry, path: 'test.txt' });

expect(formatter.toFile({ a: 'b', c: 'd' })).toEqual('a: b\nc: d');
expect(formatter.fromFile('a: b\nc: d')).toEqual({ a: 'b', c: 'd' });
});
});
17 changes: 14 additions & 3 deletions packages/netlify-cms-core/src/formats/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import yamlFormatter from './yaml';
import tomlFormatter from './toml';
import jsonFormatter from './json';
import { FrontmatterInfer, frontmatterJSON, frontmatterTOML, frontmatterYAML } from './frontmatter';
import { getCustomFormatsExtensions, getCustomFormatsFormatters } from '../lib/registry';

import type { Delimiter } from './frontmatter';
import type { Collection, EntryObject, Format } from '../types/redux';
import type { EntryValue } from '../valueObjects/Entry';
import type { Formatter } from 'netlify-cms-core';

export const frontmatterFormats = ['yaml-frontmatter', 'toml-frontmatter', 'json-frontmatter'];

Expand All @@ -23,6 +25,10 @@ export const formatExtensions = {
'yaml-frontmatter': 'md',
};

export function getFormatExtensions() {
return { ...formatExtensions, ...getCustomFormatsExtensions() };
}

export const extensionFormatters = {
yml: yamlFormatter,
yaml: yamlFormatter,
Expand All @@ -33,8 +39,8 @@ export const extensionFormatters = {
html: FrontmatterInfer,
};

function formatByName(name: Format, customDelimiter?: Delimiter) {
return {
function formatByName(name: Format, customDelimiter?: Delimiter): Formatter {
const formatters: Record<string, Formatter> = {
yml: yamlFormatter,
yaml: yamlFormatter,
toml: tomlFormatter,
Expand All @@ -43,7 +49,12 @@ function formatByName(name: Format, customDelimiter?: Delimiter) {
'json-frontmatter': frontmatterJSON(customDelimiter),
'toml-frontmatter': frontmatterTOML(customDelimiter),
'yaml-frontmatter': frontmatterYAML(customDelimiter),
}[name];
...getCustomFormatsFormatters(),
};
if (name in formatters) {
return formatters[name];
}
throw new Error(`No formatter available with name: ${name}`);
}

function frontmatterDelimiterIsList(
Expand Down
15 changes: 15 additions & 0 deletions packages/netlify-cms-core/src/lib/__tests__/registry.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ describe('registry', () => {
});
});

describe('registerCustomFormat', () => {
it('can register a custom format', () => {
const { getCustomFormats, registerCustomFormat } = require('../registry');

expect(Object.keys(getCustomFormats())).not.toContain('querystring');

registerCustomFormat('querystring', 'qs', {
fromFile: content => Object.fromEntries(new URLSearchParams(content)),
toFile: obj => new URLSearchParams(obj).toString(),
});

expect(Object.keys(getCustomFormats())).toContain('querystring');
});
});

describe('eventHandlers', () => {
const events = [
'prePublish',
Expand Down
30 changes: 30 additions & 0 deletions packages/netlify-cms-core/src/lib/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const registry = {
mediaLibraries: [],
locales: {},
eventHandlers,
formats: {},
};

export default {
Expand Down Expand Up @@ -58,6 +59,10 @@ export default {
removeEventListener,
getEventListeners,
invokeEvent,
registerCustomFormat,
getCustomFormats,
getCustomFormatsExtensions,
getCustomFormatsFormatters,
};

/**
Expand Down Expand Up @@ -280,3 +285,28 @@ export function registerLocale(locale, phrases) {
export function getLocale(locale) {
return registry.locales[locale];
}

export function registerCustomFormat(name, extension, formatter) {
registry.formats[name] = { extension, formatter };
}

export function getCustomFormats() {
return registry.formats;
}

export function getCustomFormatsExtensions() {
return Object.entries(registry.formats).reduce(function (acc, [name, { extension }]) {
return { ...acc, [name]: extension };
}, {});
}

/** @type {() => Record<string, unknown>} */
export function getCustomFormatsFormatters() {
return Object.entries(registry.formats).reduce(function (acc, [name, { formatter }]) {
return { ...acc, [name]: formatter };
}, {});
}

export function getFormatter(name) {
return registry.formats[name]?.formatter;
}
5 changes: 3 additions & 2 deletions packages/netlify-cms-core/src/reducers/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { CONFIG_SUCCESS } from '../actions/config';
import { FILES, FOLDER } from '../constants/collectionTypes';
import { COMMIT_DATE, COMMIT_AUTHOR } from '../constants/commitProps';
import { INFERABLE_FIELDS, IDENTIFIER_FIELDS, SORTABLE_FIELDS } from '../constants/fieldInference';
import { formatExtensions } from '../formats/formats';
import { getFormatExtensions } from '../formats/formats';
import { selectMediaFolder } from './entries';
import { summaryFormatter } from '../lib/formatters';

Expand Down Expand Up @@ -48,7 +48,8 @@ const selectors = {
entryExtension(collection: Collection) {
return (
collection.get('extension') ||
get(formatExtensions, collection.get('format') || 'frontmatter')
get(getFormatExtensions(), collection.get('format') || 'frontmatter') ||
get(getFormatExtensions(), 'frontmatter')
mmkal marked this conversation as resolved.
Show resolved Hide resolved
).replace(/^\./, '');
},
fields(collection: Collection) {
Expand Down
2 changes: 1 addition & 1 deletion packages/netlify-cms-core/src/types/redux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,7 @@ type i18n = StaticallyTypedRecord<{
default_locale: string;
}>;

export type Format = keyof typeof formatExtensions;
export type Format = keyof typeof formatExtensions | string;

type CollectionObject = {
name: string;
Expand Down
Loading