decap-cms-core 3.1.1 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -54,8 +54,8 @@ function buildIssueTemplate({
54
54
  let version = '';
55
55
  if (typeof DECAP_CMS_VERSION === 'string') {
56
56
  version = `decap-cms@${DECAP_CMS_VERSION}`;
57
- } else if (typeof "3.0.2" === 'string') {
58
- version = `decap-cms-app@${"3.0.2"}`;
57
+ } else if (typeof "3.0.4" === 'string') {
58
+ version = `decap-cms-app@${"3.0.4"}`;
59
59
  }
60
60
  const template = getIssueTemplate({
61
61
  version,
@@ -388,8 +388,7 @@ function getConfigSchema() {
388
388
  }
389
389
  },
390
390
  format: {
391
- type: 'string',
392
- enum: Object.keys(_formats.formatExtensions)
391
+ type: 'string'
393
392
  },
394
393
  extension: {
395
394
  type: 'string'
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.frontmatterFormats = exports.formatExtensions = exports.extensionFormatters = void 0;
7
+ exports.getFormatExtensions = getFormatExtensions;
7
8
  exports.resolveFormat = resolveFormat;
8
9
  var _get2 = _interopRequireDefault(require("lodash/get"));
9
10
  var _immutable = require("immutable");
@@ -11,7 +12,13 @@ var _yaml = _interopRequireDefault(require("./yaml"));
11
12
  var _toml = _interopRequireDefault(require("./toml"));
12
13
  var _json = _interopRequireDefault(require("./json"));
13
14
  var _frontmatter = require("./frontmatter");
15
+ var _registry = require("../lib/registry");
14
16
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
+ function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
18
+ function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
19
+ function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
20
+ function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
21
+ function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
15
22
  const frontmatterFormats = ['yaml-frontmatter', 'toml-frontmatter', 'json-frontmatter'];
16
23
  exports.frontmatterFormats = frontmatterFormats;
17
24
  const formatExtensions = {
@@ -25,6 +32,9 @@ const formatExtensions = {
25
32
  'yaml-frontmatter': 'md'
26
33
  };
27
34
  exports.formatExtensions = formatExtensions;
35
+ function getFormatExtensions() {
36
+ return _objectSpread(_objectSpread({}, formatExtensions), (0, _registry.getCustomFormatsExtensions)());
37
+ }
28
38
  const extensionFormatters = {
29
39
  yml: _yaml.default,
30
40
  yaml: _yaml.default,
@@ -36,7 +46,7 @@ const extensionFormatters = {
36
46
  };
37
47
  exports.extensionFormatters = extensionFormatters;
38
48
  function formatByName(name, customDelimiter) {
39
- return {
49
+ const formatters = _objectSpread({
40
50
  yml: _yaml.default,
41
51
  yaml: _yaml.default,
42
52
  toml: _toml.default,
@@ -45,7 +55,11 @@ function formatByName(name, customDelimiter) {
45
55
  'json-frontmatter': (0, _frontmatter.frontmatterJSON)(customDelimiter),
46
56
  'toml-frontmatter': (0, _frontmatter.frontmatterTOML)(customDelimiter),
47
57
  'yaml-frontmatter': (0, _frontmatter.frontmatterYAML)(customDelimiter)
48
- }[name];
58
+ }, (0, _registry.getCustomFormatsFormatters)());
59
+ if (name in formatters) {
60
+ return formatters[name];
61
+ }
62
+ throw new Error(`No formatter available with name: ${name}`);
49
63
  }
50
64
  function frontmatterDelimiterIsList(frontmatterDelimiter) {
51
65
  return _immutable.List.isList(frontmatterDelimiter);
@@ -5,8 +5,12 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.default = void 0;
7
7
  exports.getBackend = getBackend;
8
+ exports.getCustomFormats = getCustomFormats;
9
+ exports.getCustomFormatsExtensions = getCustomFormatsExtensions;
10
+ exports.getCustomFormatsFormatters = getCustomFormatsFormatters;
8
11
  exports.getEditorComponents = getEditorComponents;
9
12
  exports.getEventListeners = getEventListeners;
13
+ exports.getFormatter = getFormatter;
10
14
  exports.getLocale = getLocale;
11
15
  exports.getMediaLibrary = getMediaLibrary;
12
16
  exports.getPreviewStyles = getPreviewStyles;
@@ -17,6 +21,7 @@ exports.getWidgetValueSerializer = getWidgetValueSerializer;
17
21
  exports.getWidgets = getWidgets;
18
22
  exports.invokeEvent = invokeEvent;
19
23
  exports.registerBackend = registerBackend;
24
+ exports.registerCustomFormat = registerCustomFormat;
20
25
  exports.registerEditorComponent = registerEditorComponent;
21
26
  exports.registerEventListener = registerEventListener;
22
27
  exports.registerLocale = registerLocale;
@@ -60,7 +65,8 @@ const registry = {
60
65
  widgetValueSerializers: {},
61
66
  mediaLibraries: [],
62
67
  locales: {},
63
- eventHandlers
68
+ eventHandlers,
69
+ formats: {}
64
70
  };
65
71
  var _default = {
66
72
  registerPreviewStyle,
@@ -86,7 +92,11 @@ var _default = {
86
92
  registerEventListener,
87
93
  removeEventListener,
88
94
  getEventListeners,
89
- invokeEvent
95
+ invokeEvent,
96
+ registerCustomFormat,
97
+ getCustomFormats,
98
+ getCustomFormatsExtensions,
99
+ getCustomFormatsFormatters
90
100
  };
91
101
  /**
92
102
  * Preview Styles
@@ -319,4 +329,37 @@ function registerLocale(locale, phrases) {
319
329
  }
320
330
  function getLocale(locale) {
321
331
  return registry.locales[locale];
332
+ }
333
+ function registerCustomFormat(name, extension, formatter) {
334
+ registry.formats[name] = {
335
+ extension,
336
+ formatter
337
+ };
338
+ }
339
+ function getCustomFormats() {
340
+ return registry.formats;
341
+ }
342
+ function getCustomFormatsExtensions() {
343
+ return Object.entries(registry.formats).reduce(function (acc, [name, {
344
+ extension
345
+ }]) {
346
+ return _objectSpread(_objectSpread({}, acc), {}, {
347
+ [name]: extension
348
+ });
349
+ }, {});
350
+ }
351
+
352
+ /** @type {() => Record<string, unknown>} */
353
+ function getCustomFormatsFormatters() {
354
+ return Object.entries(registry.formats).reduce(function (acc, [name, {
355
+ formatter
356
+ }]) {
357
+ return _objectSpread(_objectSpread({}, acc), {}, {
358
+ [name]: formatter
359
+ });
360
+ }, {});
361
+ }
362
+ function getFormatter(name) {
363
+ var _registry$formats$nam;
364
+ return (_registry$formats$nam = registry.formats[name]) === null || _registry$formats$nam === void 0 ? void 0 : _registry$formats$nam.formatter;
322
365
  }
@@ -69,7 +69,11 @@ function collections(state = defaultState, action) {
69
69
  const selectors = {
70
70
  [_collectionTypes.FOLDER]: {
71
71
  entryExtension(collection) {
72
- return (collection.get('extension') || (0, _get2.default)(_formats.formatExtensions, collection.get('format') || 'frontmatter')).replace(/^\./, '');
72
+ const ext = collection.get('extension') || (0, _get2.default)((0, _formats.getFormatExtensions)(), collection.get('format') || 'frontmatter');
73
+ if (!ext) {
74
+ throw new Error(`No extension found for format ${collection.get('format')}`);
75
+ }
76
+ return ext.replace(/^\./, '');
73
77
  },
74
78
  fields(collection) {
75
79
  return collection.get('fields');
package/index.d.ts CHANGED
@@ -36,15 +36,7 @@ declare module 'decap-cms-core' {
36
36
  value: any;
37
37
  }
38
38
 
39
- export type CmsCollectionFormatType =
40
- | 'yml'
41
- | 'yaml'
42
- | 'toml'
43
- | 'json'
44
- | 'frontmatter'
45
- | 'yaml-frontmatter'
46
- | 'toml-frontmatter'
47
- | 'json-frontmatter';
39
+ export type CmsCollectionFormatType = string;
48
40
 
49
41
  export type CmsAuthScope = 'repo' | 'public_repo';
50
42
 
@@ -501,6 +493,11 @@ declare module 'decap-cms-core' {
501
493
 
502
494
  export type CmsLocalePhrases = any; // TODO: type properly
503
495
 
496
+ export type Formatter = {
497
+ fromFile(content: string): unknown;
498
+ toFile(data: object, sortedKeys?: string[], comments?: Record<string, string>): string;
499
+ };
500
+
504
501
  export interface CmsRegistry {
505
502
  backends: {
506
503
  [name: string]: CmsRegistryBackend;
@@ -520,6 +517,9 @@ declare module 'decap-cms-core' {
520
517
  locales: {
521
518
  [name: string]: CmsLocalePhrases;
522
519
  };
520
+ formats: {
521
+ [name: string]: Formatter;
522
+ };
523
523
  }
524
524
 
525
525
  type GetAssetFunction = (asset: string) => {
@@ -579,6 +579,7 @@ declare module 'decap-cms-core' {
579
579
  serializer: CmsWidgetValueSerializer,
580
580
  ) => void;
581
581
  resolveWidget: (name: string) => CmsWidget | undefined;
582
+ registerCustomFormat: (name: string, extension: string, formatter: Formatter) => void;
582
583
  }
583
584
 
584
585
  export const DecapCmsCore: CMS;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "decap-cms-core",
3
3
  "description": "Decap CMS core application, see decap-cms package for the main distribution.",
4
- "version": "3.1.1",
4
+ "version": "3.2.1",
5
5
  "repository": "https://github.com/decaporg/decap-cms/tree/master/packages/decap-cms-core",
6
6
  "bugs": "https://github.com/decaporg/decap-cms/issues",
7
7
  "module": "dist/esm/index.js",
@@ -98,5 +98,5 @@
98
98
  "@types/url-join": "^4.0.0",
99
99
  "redux-mock-store": "^1.5.3"
100
100
  },
101
- "gitHead": "22632906a6d5bc2b889535d30fdf49cd8eb19883"
101
+ "gitHead": "25ef3074bd4fdc285f2245f1e10100316b3ff7db"
102
102
  }
@@ -451,6 +451,7 @@ export class EditorToolbar extends React.Component {
451
451
  <ToolbarDropdown
452
452
  dropdownTopOverlap="40px"
453
453
  dropdownWidth="150px"
454
+ key="td-publish-create"
454
455
  renderButton={() => (
455
456
  <PublishedToolbarButton>
456
457
  {isPersisting
@@ -200,7 +200,7 @@ exports[`EditorToolbar should render normal save button 1`] = `
200
200
  class="emotion-21 emotion-22"
201
201
  >
202
202
  <mock-link
203
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
203
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
204
204
  to=""
205
205
  >
206
206
  <div
@@ -463,7 +463,7 @@ exports[`EditorToolbar should render normal save button 2`] = `
463
463
  class="emotion-21 emotion-22"
464
464
  >
465
465
  <mock-link
466
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
466
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
467
467
  to=""
468
468
  >
469
469
  <div
@@ -695,7 +695,7 @@ exports[`EditorToolbar should render with default props 1`] = `
695
695
  class="emotion-18 emotion-19"
696
696
  >
697
697
  <mock-link
698
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
698
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
699
699
  to=""
700
700
  >
701
701
  <div
@@ -980,7 +980,7 @@ exports[`EditorToolbar should render with status=draft,useOpenAuthoring=false 1`
980
980
  class="emotion-23 emotion-24"
981
981
  >
982
982
  <mock-link
983
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
983
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
984
984
  to=""
985
985
  >
986
986
  <div
@@ -1310,7 +1310,7 @@ exports[`EditorToolbar should render with status=draft,useOpenAuthoring=true 1`]
1310
1310
  class="emotion-30 emotion-31"
1311
1311
  >
1312
1312
  <mock-link
1313
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
1313
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
1314
1314
  to=""
1315
1315
  >
1316
1316
  <div
@@ -1634,7 +1634,7 @@ exports[`EditorToolbar should render with status=pending_publish,useOpenAuthorin
1634
1634
  class="emotion-23 emotion-24"
1635
1635
  >
1636
1636
  <mock-link
1637
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
1637
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
1638
1638
  to=""
1639
1639
  >
1640
1640
  <div
@@ -1946,7 +1946,7 @@ exports[`EditorToolbar should render with status=pending_publish,useOpenAuthorin
1946
1946
  class="emotion-28 emotion-29"
1947
1947
  >
1948
1948
  <mock-link
1949
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
1949
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
1950
1950
  to=""
1951
1951
  >
1952
1952
  <div
@@ -2265,7 +2265,7 @@ exports[`EditorToolbar should render with status=pending_review,useOpenAuthoring
2265
2265
  class="emotion-23 emotion-24"
2266
2266
  >
2267
2267
  <mock-link
2268
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
2268
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
2269
2269
  to=""
2270
2270
  >
2271
2271
  <div
@@ -2595,7 +2595,7 @@ exports[`EditorToolbar should render with status=pending_review,useOpenAuthoring
2595
2595
  class="emotion-30 emotion-31"
2596
2596
  >
2597
2597
  <mock-link
2598
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
2598
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
2599
2599
  to=""
2600
2600
  >
2601
2601
  <div
@@ -2862,7 +2862,7 @@ exports[`EditorToolbar should render with workflow controls hasUnpublishedChange
2862
2862
  class="emotion-18 emotion-19"
2863
2863
  >
2864
2864
  <mock-link
2865
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
2865
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
2866
2866
  to=""
2867
2867
  >
2868
2868
  <div
@@ -3064,7 +3064,7 @@ exports[`EditorToolbar should render with workflow controls hasUnpublishedChange
3064
3064
  class="emotion-16 emotion-17"
3065
3065
  >
3066
3066
  <mock-link
3067
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
3067
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
3068
3068
  to=""
3069
3069
  >
3070
3070
  <div
@@ -3286,7 +3286,7 @@ exports[`EditorToolbar should render with workflow controls hasUnpublishedChange
3286
3286
  class="emotion-18 emotion-19"
3287
3287
  >
3288
3288
  <mock-link
3289
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
3289
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
3290
3290
  to=""
3291
3291
  >
3292
3292
  <div
@@ -3513,7 +3513,7 @@ exports[`EditorToolbar should render with workflow controls hasUnpublishedChange
3513
3513
  class="emotion-18 emotion-19"
3514
3514
  >
3515
3515
  <mock-link
3516
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
3516
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
3517
3517
  to=""
3518
3518
  >
3519
3519
  <div
@@ -3740,7 +3740,7 @@ exports[`EditorToolbar should render with workflow controls hasUnpublishedChange
3740
3740
  class="emotion-18 emotion-19"
3741
3741
  >
3742
3742
  <mock-link
3743
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
3743
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
3744
3744
  to=""
3745
3745
  >
3746
3746
  <div
@@ -3967,7 +3967,7 @@ exports[`EditorToolbar should render with workflow controls hasUnpublishedChange
3967
3967
  class="emotion-18 emotion-19"
3968
3968
  >
3969
3969
  <mock-link
3970
- classname="css-16gdrpq-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
3970
+ classname="css-h6rugo-ToolbarSectionBackLink-toolbarSection e1d2l9mo8"
3971
3971
  to=""
3972
3972
  >
3973
3973
  <div
@@ -8,7 +8,7 @@ import {
8
8
  import ajvErrors from 'ajv-errors';
9
9
  import uuid from 'uuid/v4';
10
10
 
11
- import { formatExtensions, frontmatterFormats, extensionFormatters } from '../formats/formats';
11
+ import { frontmatterFormats, extensionFormatters } from '../formats/formats';
12
12
  import { getWidgets } from '../lib/registry';
13
13
  import { I18N_STRUCTURE, I18N_FIELD } from '../lib/i18n';
14
14
 
@@ -231,7 +231,7 @@ function getConfigSchema() {
231
231
  preview: { type: 'boolean' },
232
232
  },
233
233
  },
234
- format: { type: 'string', enum: Object.keys(formatExtensions) },
234
+ format: { type: 'string' },
235
235
  extension: { type: 'string' },
236
236
  frontmatter_delimiter: {
237
237
  type: ['string', 'array'],
@@ -0,0 +1,87 @@
1
+ import { Map } from 'immutable';
2
+
3
+ import { extensionFormatters, resolveFormat } from '../formats';
4
+ import { registerCustomFormat } from '../../lib/registry';
5
+
6
+ describe('custom formats', () => {
7
+ const testEntry = {
8
+ collection: 'testCollection',
9
+ data: { x: 1 },
10
+ isModification: false,
11
+ label: 'testLabel',
12
+ mediaFiles: [],
13
+ meta: {},
14
+ newRecord: true,
15
+ partial: false,
16
+ path: 'testPath1',
17
+ raw: 'testRaw',
18
+ slug: 'testSlug',
19
+ author: 'testAuthor',
20
+ updatedOn: 'testUpdatedOn',
21
+ };
22
+ it('resolves builtint formats', () => {
23
+ const collection = Map({
24
+ name: 'posts',
25
+ });
26
+ expect(resolveFormat(collection, { ...testEntry, path: 'test.yml' })).toEqual(
27
+ extensionFormatters.yml,
28
+ );
29
+ expect(resolveFormat(collection, { ...testEntry, path: 'test.yaml' })).toEqual(
30
+ extensionFormatters.yml,
31
+ );
32
+ expect(resolveFormat(collection, { ...testEntry, path: 'test.toml' })).toEqual(
33
+ extensionFormatters.toml,
34
+ );
35
+ expect(resolveFormat(collection, { ...testEntry, path: 'test.json' })).toEqual(
36
+ extensionFormatters.json,
37
+ );
38
+ expect(resolveFormat(collection, { ...testEntry, path: 'test.md' })).toEqual(
39
+ extensionFormatters.md,
40
+ );
41
+ expect(resolveFormat(collection, { ...testEntry, path: 'test.markdown' })).toEqual(
42
+ extensionFormatters.markdown,
43
+ );
44
+ expect(resolveFormat(collection, { ...testEntry, path: 'test.html' })).toEqual(
45
+ extensionFormatters.html,
46
+ );
47
+ });
48
+
49
+ it('resolves custom format', () => {
50
+ registerCustomFormat('txt-querystring', 'txt', {
51
+ fromFile: file => Object.fromEntries(new URLSearchParams(file)),
52
+ toFile: value => new URLSearchParams(value).toString(),
53
+ });
54
+
55
+ const collection = Map({
56
+ name: 'posts',
57
+ format: 'txt-querystring',
58
+ });
59
+
60
+ const formatter = resolveFormat(collection, { ...testEntry, path: 'test.txt' });
61
+
62
+ expect(formatter.toFile({ foo: 'bar' })).toEqual('foo=bar');
63
+ expect(formatter.fromFile('foo=bar')).toEqual({ foo: 'bar' });
64
+ });
65
+
66
+ it('can override existing formatters', () => {
67
+ // simplified version of a more realistic use case: using a different yaml library like js-yaml
68
+ // to make netlify-cms play nice with other tools that edit content and spit out yaml
69
+ registerCustomFormat('bad-yaml', 'yml', {
70
+ fromFile: file => Object.fromEntries(file.split('\n').map(line => line.split(': '))),
71
+ toFile: value =>
72
+ Object.entries(value)
73
+ .map(([k, v]) => `${k}: ${v}`)
74
+ .join('\n'),
75
+ });
76
+
77
+ const collection = Map({
78
+ name: 'posts',
79
+ format: 'bad-yaml',
80
+ });
81
+
82
+ const formatter = resolveFormat(collection, { ...testEntry, path: 'test.txt' });
83
+
84
+ expect(formatter.toFile({ a: 'b', c: 'd' })).toEqual('a: b\nc: d');
85
+ expect(formatter.fromFile('a: b\nc: d')).toEqual({ a: 'b', c: 'd' });
86
+ });
87
+ });
@@ -5,10 +5,12 @@ import yamlFormatter from './yaml';
5
5
  import tomlFormatter from './toml';
6
6
  import jsonFormatter from './json';
7
7
  import { FrontmatterInfer, frontmatterJSON, frontmatterTOML, frontmatterYAML } from './frontmatter';
8
+ import { getCustomFormatsExtensions, getCustomFormatsFormatters } from '../lib/registry';
8
9
 
9
10
  import type { Delimiter } from './frontmatter';
10
11
  import type { Collection, EntryObject, Format } from '../types/redux';
11
12
  import type { EntryValue } from '../valueObjects/Entry';
13
+ import type { Formatter } from 'decap-cms-core';
12
14
 
13
15
  export const frontmatterFormats = ['yaml-frontmatter', 'toml-frontmatter', 'json-frontmatter'];
14
16
 
@@ -23,6 +25,10 @@ export const formatExtensions = {
23
25
  'yaml-frontmatter': 'md',
24
26
  };
25
27
 
28
+ export function getFormatExtensions() {
29
+ return { ...formatExtensions, ...getCustomFormatsExtensions() };
30
+ }
31
+
26
32
  export const extensionFormatters = {
27
33
  yml: yamlFormatter,
28
34
  yaml: yamlFormatter,
@@ -33,8 +39,8 @@ export const extensionFormatters = {
33
39
  html: FrontmatterInfer,
34
40
  };
35
41
 
36
- function formatByName(name: Format, customDelimiter?: Delimiter) {
37
- return {
42
+ function formatByName(name: Format, customDelimiter?: Delimiter): Formatter {
43
+ const formatters: Record<string, Formatter> = {
38
44
  yml: yamlFormatter,
39
45
  yaml: yamlFormatter,
40
46
  toml: tomlFormatter,
@@ -43,7 +49,12 @@ function formatByName(name: Format, customDelimiter?: Delimiter) {
43
49
  'json-frontmatter': frontmatterJSON(customDelimiter),
44
50
  'toml-frontmatter': frontmatterTOML(customDelimiter),
45
51
  'yaml-frontmatter': frontmatterYAML(customDelimiter),
46
- }[name];
52
+ ...getCustomFormatsFormatters(),
53
+ };
54
+ if (name in formatters) {
55
+ return formatters[name];
56
+ }
57
+ throw new Error(`No formatter available with name: ${name}`);
47
58
  }
48
59
 
49
60
  function frontmatterDelimiterIsList(
@@ -46,6 +46,21 @@ describe('registry', () => {
46
46
  });
47
47
  });
48
48
 
49
+ describe('registerCustomFormat', () => {
50
+ it('can register a custom format', () => {
51
+ const { getCustomFormats, registerCustomFormat } = require('../registry');
52
+
53
+ expect(Object.keys(getCustomFormats())).not.toContain('querystring');
54
+
55
+ registerCustomFormat('querystring', 'qs', {
56
+ fromFile: content => Object.fromEntries(new URLSearchParams(content)),
57
+ toFile: obj => new URLSearchParams(obj).toString(),
58
+ });
59
+
60
+ expect(Object.keys(getCustomFormats())).toContain('querystring');
61
+ });
62
+ });
63
+
49
64
  describe('eventHandlers', () => {
50
65
  const events = [
51
66
  'prePublish',
@@ -31,6 +31,7 @@ const registry = {
31
31
  mediaLibraries: [],
32
32
  locales: {},
33
33
  eventHandlers,
34
+ formats: {},
34
35
  };
35
36
 
36
37
  export default {
@@ -58,6 +59,10 @@ export default {
58
59
  removeEventListener,
59
60
  getEventListeners,
60
61
  invokeEvent,
62
+ registerCustomFormat,
63
+ getCustomFormats,
64
+ getCustomFormatsExtensions,
65
+ getCustomFormatsFormatters,
61
66
  };
62
67
 
63
68
  /**
@@ -280,3 +285,28 @@ export function registerLocale(locale, phrases) {
280
285
  export function getLocale(locale) {
281
286
  return registry.locales[locale];
282
287
  }
288
+
289
+ export function registerCustomFormat(name, extension, formatter) {
290
+ registry.formats[name] = { extension, formatter };
291
+ }
292
+
293
+ export function getCustomFormats() {
294
+ return registry.formats;
295
+ }
296
+
297
+ export function getCustomFormatsExtensions() {
298
+ return Object.entries(registry.formats).reduce(function (acc, [name, { extension }]) {
299
+ return { ...acc, [name]: extension };
300
+ }, {});
301
+ }
302
+
303
+ /** @type {() => Record<string, unknown>} */
304
+ export function getCustomFormatsFormatters() {
305
+ return Object.entries(registry.formats).reduce(function (acc, [name, { formatter }]) {
306
+ return { ...acc, [name]: formatter };
307
+ }, {});
308
+ }
309
+
310
+ export function getFormatter(name) {
311
+ return registry.formats[name]?.formatter;
312
+ }
@@ -7,7 +7,7 @@ import { CONFIG_SUCCESS } from '../actions/config';
7
7
  import { FILES, FOLDER } from '../constants/collectionTypes';
8
8
  import { COMMIT_DATE, COMMIT_AUTHOR } from '../constants/commitProps';
9
9
  import { INFERABLE_FIELDS, IDENTIFIER_FIELDS, SORTABLE_FIELDS } from '../constants/fieldInference';
10
- import { formatExtensions } from '../formats/formats';
10
+ import { getFormatExtensions } from '../formats/formats';
11
11
  import { selectMediaFolder } from './entries';
12
12
  import { summaryFormatter } from '../lib/formatters';
13
13
 
@@ -46,10 +46,14 @@ function collections(state = defaultState, action: ConfigAction) {
46
46
  const selectors = {
47
47
  [FOLDER]: {
48
48
  entryExtension(collection: Collection) {
49
- return (
49
+ const ext =
50
50
  collection.get('extension') ||
51
- get(formatExtensions, collection.get('format') || 'frontmatter')
52
- ).replace(/^\./, '');
51
+ get(getFormatExtensions(), collection.get('format') || 'frontmatter');
52
+ if (!ext) {
53
+ throw new Error(`No extension found for format ${collection.get('format')}`);
54
+ }
55
+
56
+ return ext.replace(/^\./, '');
53
57
  },
54
58
  fields(collection: Collection) {
55
59
  return collection.get('fields');
@@ -599,7 +599,7 @@ type i18n = StaticallyTypedRecord<{
599
599
  default_locale: string;
600
600
  }>;
601
601
 
602
- export type Format = keyof typeof formatExtensions;
602
+ export type Format = keyof typeof formatExtensions | string;
603
603
 
604
604
  type CollectionObject = {
605
605
  name: string;