From fde0c5a9a776dc814bbe3a483aa286f46f45d98d Mon Sep 17 00:00:00 2001 From: Erez Rokah Date: Tue, 14 Jan 2020 20:20:52 +0200 Subject: [PATCH] fix(widget-markdown): ensure remarkToSlate result matches slate schema (#3085) --- .../__snapshots__/parser.spec.js.snap | 14 ++++ .../MarkdownControl/__tests__/parser.spec.js | 46 +++++++++++ .../serializers/__tests__/remarkSlate.spec.js | 77 +++++++++++++++++++ .../src/serializers/remarkSlate.js | 52 ++++++++++++- 4 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkSlate.spec.js diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/__snapshots__/parser.spec.js.snap b/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/__snapshots__/parser.spec.js.snap index f4c66455..36df8dab 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/__snapshots__/parser.spec.js.snap +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/__snapshots__/parser.spec.js.snap @@ -476,6 +476,10 @@ more important, there there are only so many sizes that you can use.", "nodes": Array [ Object { "nodes": Array [ + Object { + "object": "text", + "text": "", + }, Object { "data": Object { "title": null, @@ -506,6 +510,10 @@ more important, there there are only so many sizes that you can use.", "nodes": Array [ Object { "nodes": Array [ + Object { + "object": "text", + "text": "", + }, Object { "data": Object { "title": null, @@ -557,6 +565,12 @@ more important, there there are only so many sizes that you can use.", "type": "paragraph", }, Object { + "nodes": Array [ + Object { + "object": "text", + "text": "", + }, + ], "object": "block", "type": "thematic-break", }, diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/parser.spec.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/parser.spec.js index 748e0d7f..fa20fad5 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/parser.spec.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/parser.spec.js @@ -279,6 +279,12 @@ Object { "type": "heading-one", }, Object { + "nodes": Array [ + Object { + "object": "text", + "text": "", + }, + ], "object": "block", "type": "thematic-break", }, @@ -321,6 +327,12 @@ Object { "type": "heading-one", }, Object { + "nodes": Array [ + Object { + "object": "text", + "text": "", + }, + ], "object": "block", "type": "thematic-break", }, @@ -357,6 +369,12 @@ Object { }, Object { "data": undefined, + "nodes": Array [ + Object { + "object": "text", + "text": "", + }, + ], "object": "inline", "type": "break", }, @@ -384,15 +402,29 @@ Object { "nodes": Array [ Object { "nodes": Array [ + Object { + "object": "text", + "text": "", + }, Object { "data": Object { "alt": "super", "title": null, "url": "duper.jpg", }, + "nodes": Array [ + Object { + "object": "text", + "text": "", + }, + ], "object": "inline", "type": "image", }, + Object { + "object": "text", + "text": "", + }, ], "object": "block", "type": "paragraph", @@ -638,15 +670,29 @@ Object { "nodes": Array [ Object { "nodes": Array [ + Object { + "object": "text", + "text": "", + }, Object { "data": Object { "alt": "test", "title": null, "url": "test.png", }, + "nodes": Array [ + Object { + "object": "text", + "text": "", + }, + ], "object": "inline", "type": "image", }, + Object { + "object": "text", + "text": "", + }, ], "object": "block", "type": "paragraph", diff --git a/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkSlate.spec.js b/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkSlate.spec.js new file mode 100644 index 00000000..4d6bf4dc --- /dev/null +++ b/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkSlate.spec.js @@ -0,0 +1,77 @@ +import { wrapInlinesWithTexts } from '../remarkSlate'; +describe('remarkSlate', () => { + describe('wrapInlinesWithTexts', () => { + it('should handle empty array', () => { + const children = []; + expect(wrapInlinesWithTexts(children)).toBe(children); + }); + + it('should wrap single inline node with texts', () => { + expect(wrapInlinesWithTexts([{ object: 'inline' }])).toEqual([ + { object: 'text', text: '' }, + { object: 'inline' }, + { object: 'text', text: '' }, + ]); + }); + + it('should insert text before inline', () => { + expect(wrapInlinesWithTexts([{ object: 'inline' }, { object: 'text', text: '' }])).toEqual([ + { object: 'text', text: '' }, + { object: 'inline' }, + { object: 'text', text: '' }, + ]); + }); + + it('should insert text after inline', () => { + expect(wrapInlinesWithTexts([{ object: 'text', text: '' }, { object: 'inline' }])).toEqual([ + { object: 'text', text: '' }, + { object: 'inline' }, + { object: 'text', text: '' }, + ]); + }); + + it('should not modify valid children array', () => { + const children = [ + { object: 'text', text: '' }, + { object: 'inline' }, + { object: 'text', text: '' }, + ]; + expect(wrapInlinesWithTexts(children)).toBe(children); + }); + + it('should wrap inlines with text nodes', () => { + expect( + wrapInlinesWithTexts([ + { object: 'inline' }, + { object: 'other' }, + { object: 'inline' }, + { object: 'inline' }, + { object: 'other' }, + { object: 'text', text: 'hello' }, + { object: 'inline' }, + { object: 'inline' }, + { object: 'text', text: 'world' }, + { object: 'inline' }, + ]), + ).toEqual([ + { object: 'text', text: '' }, + { object: 'inline' }, + { object: 'text', text: '' }, + { object: 'other' }, + { object: 'text', text: '' }, + { object: 'inline' }, + { object: 'text', text: '' }, + { object: 'inline' }, + { object: 'text', text: '' }, + { object: 'other' }, + { object: 'text', text: 'hello' }, + { object: 'inline' }, + { object: 'text', text: '' }, + { object: 'inline' }, + { object: 'text', text: 'world' }, + { object: 'inline' }, + { object: 'text', text: '' }, + ]); + }); + }); +}); diff --git a/packages/netlify-cms-widget-markdown/src/serializers/remarkSlate.js b/packages/netlify-cms-widget-markdown/src/serializers/remarkSlate.js index b88b2c3e..da9dbf96 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/remarkSlate.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/remarkSlate.js @@ -28,6 +28,42 @@ const markMap = { inlineCode: 'code', }; +const isInline = node => node.object === 'inline'; +const isText = node => node.object === 'text'; + +export const wrapInlinesWithTexts = children => { + if (children.length <= 0) { + return children; + } + + const insertLocations = []; + let prev = children[0]; + if (isInline(prev)) { + insertLocations.push(0); + } + + for (let i = 1; i < children.length; i++) { + const current = children[i]; + if (isInline(prev) && !isText(current)) { + insertLocations.push(i); + } else if (!isText(prev) && isInline(current)) { + insertLocations.push(i); + } + + prev = current; + } + + if (isInline(prev)) { + insertLocations.push(children.length); + } + + for (let i = 0; i < insertLocations.length; i++) { + children.splice(insertLocations[i] + i, 0, { object: 'text', text: '' }); + } + + return children; +}; + /** * A Remark plugin for converting an MDAST to Slate Raw AST. Remark plugins * return a `transformNode` function that receives the MDAST as it's first argument. @@ -43,11 +79,16 @@ export default function remarkToSlate({ voidCodeBlock } = {}) { * translate from MDAST to Slate, such as definitions for link/image * references or footnotes. */ - const children = + let children = !['strong', 'emphasis', 'delete'].includes(node.type) && !isEmpty(node.children) && flatMap(node.children, transformNode).filter(val => val); + if (Array.isArray(children)) { + // Ensure that inline nodes are surrounded by text nodes to conform to slate schema + children = wrapInlinesWithTexts(children); + } + /** * Run individual nodes through the conversion factory. */ @@ -71,8 +112,10 @@ export default function remarkToSlate({ voidCodeBlock } = {}) { nodes = undefined; } + // Ensure block nodes have at least one text child to conform to slate schema + const children = isEmpty(nodes) ? [createText('')] : nodes; const node = { object: 'block', type, ...props }; - return addNodes(node, nodes); + return addNodes(node, children); } /** @@ -80,7 +123,10 @@ export default function remarkToSlate({ voidCodeBlock } = {}) { */ function createInline(type, props = {}, nodes) { const node = { object: 'inline', type, ...props }; - return addNodes(node, nodes); + + // Ensure inline nodes have at least one text child to conform to slate schema + const children = isEmpty(nodes) ? [createText('')] : nodes; + return addNodes(node, children); } /**