diff --git a/packages/netlify-cms-core/src/formats/__tests__/frontmatter.spec.js b/packages/netlify-cms-core/src/formats/__tests__/frontmatter.spec.js index 2ea344523375..eef5d7a88827 100644 --- a/packages/netlify-cms-core/src/formats/__tests__/frontmatter.spec.js +++ b/packages/netlify-cms-core/src/formats/__tests__/frontmatter.spec.js @@ -179,14 +179,14 @@ describe('Frontmatter', () => { 'title: YAML', '---', 'Some content', - 'On another line\n', + 'On another line', ].join('\n'), ); }); it('should stringify YAML with missing body', () => { expect(FrontmatterInfer.toFile({ tags: ['front matter', 'yaml'], title: 'YAML' })).toEqual( - ['---', 'tags:', ' - front matter', ' - yaml', 'title: YAML', '---', '', ''].join('\n'), + ['---', 'tags:', ' - front matter', ' - yaml', 'title: YAML', '---', ''].join('\n'), ); }); @@ -206,7 +206,7 @@ describe('Frontmatter', () => { 'title: YAML', '---', 'Some content', - 'On another line\n', + 'On another line', ].join('\n'), ); }); @@ -227,7 +227,7 @@ describe('Frontmatter', () => { 'title: YAML', '~~~', 'Some content', - 'On another line\n', + 'On another line', ].join('\n'), ); }); @@ -248,7 +248,7 @@ describe('Frontmatter', () => { 'title: YAML', '^^^', 'Some content', - 'On another line\n', + 'On another line', ].join('\n'), ); }); @@ -267,7 +267,7 @@ describe('Frontmatter', () => { 'title = "TOML"', '+++', 'Some content', - 'On another line\n', + 'On another line', ].join('\n'), ); }); @@ -286,7 +286,7 @@ describe('Frontmatter', () => { 'title = "TOML"', '~~~', 'Some content', - 'On another line\n', + 'On another line', ].join('\n'), ); }); @@ -308,7 +308,7 @@ describe('Frontmatter', () => { ' "title": "JSON"', '}', 'Some content', - 'On another line\n', + 'On another line', ].join('\n'), ); }); @@ -330,8 +330,24 @@ describe('Frontmatter', () => { ' "title": "JSON"', '~~~', 'Some content', - 'On another line\n', + 'On another line', ].join('\n'), ); }); + + it('should trim last line break if added by grey-matter', () => { + expect( + frontmatterYAML().toFile({ + body: 'noLineBreak', + }), + ).toEqual('noLineBreak'); + }); + + it('should not trim last line break if not added by grey-matter', () => { + expect( + frontmatterYAML().toFile({ + body: 'withLineBreak\n', + }), + ).toEqual('withLineBreak\n'); + }); }); diff --git a/packages/netlify-cms-core/src/formats/frontmatter.js b/packages/netlify-cms-core/src/formats/frontmatter.js index 309f32ce5103..a9700c0a64fa 100644 --- a/packages/netlify-cms-core/src/formats/frontmatter.js +++ b/packages/netlify-cms-core/src/formats/frontmatter.js @@ -72,7 +72,7 @@ class FrontmatterFormatter { const result = matter(content, { engines: parsers, ...format }); return { ...result.data, - ...(result.content.trim() && { body: result.content }), + body: result.content, }; } @@ -83,8 +83,13 @@ class FrontmatterFormatter { 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 - return matter.stringify(body, meta, { engines: parsers, sortedKeys, ...format }); + const file = matter.stringify(body, meta, { engines: parsers, sortedKeys, ...format }); + return trimLastLineBreak && file.slice(-1) === '\n' ? file.substring(0, file.length - 1) : file; } } diff --git a/packages/netlify-cms-core/src/reducers/entryDraft.js b/packages/netlify-cms-core/src/reducers/entryDraft.js index 48ed07ecee0b..4f0fb76dd4e7 100644 --- a/packages/netlify-cms-core/src/reducers/entryDraft.js +++ b/packages/netlify-cms-core/src/reducers/entryDraft.js @@ -31,6 +31,13 @@ const initialState = Map({ key: '', }); +const isChanged = (entry, value) => { + if (entry && !entry.get('data').equals(value)) { + return true; + } + return false; +}; + const entryDraftReducer = (state = Map(), action) => { switch (action.type) { case DRAFT_CREATE_FROM_ENTRY: @@ -89,18 +96,19 @@ const entryDraftReducer = (state = Map(), action) => { return state.set('localBackup', newState); } case DRAFT_CHANGE_FIELD: { - const publishedEntry = action.payload.publishedEntry || Map(); - const unpublishedEntry = action.payload.unpublishedEntry || Map(); - let newState = state.withMutations(state => { - state.setIn(['entry', 'data', action.payload.field], action.payload.value); + const publishedEntry = action.payload.publishedEntry; + const unpublishedEntry = action.payload.unpublishedEntry; + const { value } = action.payload; + const newState = state.withMutations(state => { state.mergeDeepIn(['fieldsMetaData'], fromJS(action.payload.metadata)); - state.set('hasChanged', true); + state.setIn(['entry', 'data', action.payload.field], value); + const newData = state.getIn(['entry', 'data']); + if (isChanged(publishedEntry, newData) || isChanged(unpublishedEntry, newData)) { + state.set('hasChanged', true); + } else { + state.set('hasChanged', false); + } }); - const newStateData = newState.getIn(['entry', 'data']); - - (newStateData.equals(unpublishedEntry.get('data')) || - newStateData.equals(publishedEntry.get('data'))) && - (newState = newState.set('hasChanged', false)); return newState; }