diff --git a/src/actions/editorialWorkflow.js b/src/actions/editorialWorkflow.js index 7c1afdd3..98e48532 100644 --- a/src/actions/editorialWorkflow.js +++ b/src/actions/editorialWorkflow.js @@ -1,5 +1,6 @@ import uuid from 'uuid'; import { actions as notifActions } from 'redux-notifications'; +import { serializeValues } from '../lib/serializeEntryValues'; import { closeEntry } from './editor'; import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; import { currentBackend } from '../backends/backend'; diff --git a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js index 1ef4ae0f..7983afb4 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/RawEditor/index.js @@ -1,6 +1,5 @@ import React, { PropTypes } from 'react'; import { Editor as Slate, Plain } from 'slate'; -import { markdownToRemark, remarkToMarkdown } from '../../serializers'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; import styles from './index.css'; @@ -8,14 +7,8 @@ import styles from './index.css'; export default class RawEditor extends React.Component { constructor(props) { super(props); - /** - * The value received is a Remark AST (MDAST), and must be stringified - * to plain text before Slate's Plain serializer can convert it to the - * Slate AST. - */ - const value = remarkToMarkdown(this.props.value); this.state = { - editorState: Plain.deserialize(value || ''), + editorState: Plain.deserialize(this.props.value || ''), }; } @@ -29,13 +22,11 @@ export default class RawEditor extends React.Component { /** * When the document value changes, serialize from Slate's AST back to plain - * text (which is Markdown), and then deserialize from that to a Remark MDAST, - * before passing up as the new value. + * text (which is Markdown) and pass that up as the new value. */ handleDocumentChange = (doc, editorState) => { const value = Plain.serialize(editorState); - const mdast = markdownToRemark(value); - this.props.onChange(mdast); + this.props.onChange(value); }; /** @@ -79,5 +70,5 @@ export default class RawEditor extends React.Component { RawEditor.propTypes = { onChange: PropTypes.func.isRequired, onMode: PropTypes.func.isRequired, - value: PropTypes.object, + value: PropTypes.string, }; diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/parser.spec.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/parser.spec.js index 6700eacf..f549b366 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/parser.spec.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/__tests__/parser.spec.js @@ -1,8 +1,9 @@ import { fromJS } from 'immutable'; -import { markdownToRemark, remarkToSlate } from '../../../serializers'; +import { markdownToSlate } from '../../../serializers'; -// Temporary plugins test, uses preloaded plugins from ../parser -// TODO: make the parser more testable +const parser = markdownToSlate; + +// Temporary plugins test const testPlugins = fromJS([ { label: 'Image', @@ -44,8 +45,6 @@ const testPlugins = fromJS([ }, ]); -const parser = markdown => remarkToSlate(markdownToRemark(markdown)); - describe("Compile markdown to Slate Raw AST", () => { it("should compile simple markdown", () => { const value = ` diff --git a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js index 33fbcd26..3a67ad7a 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/VisualEditor/index.js @@ -1,7 +1,7 @@ import React, { Component, PropTypes } from 'react'; import { get, isEmpty } from 'lodash'; import { Editor as Slate, Raw, Block, Text } from 'slate'; -import { slateToRemark, remarkToSlate, htmlToSlate } from '../../serializers'; +import { slateToMarkdown, markdownToSlate, htmlToSlate } from '../../serializers'; import registry from '../../../../../lib/registry'; import Toolbar from '../Toolbar/Toolbar'; import { Sticky } from '../../../../UI/Sticky/Sticky'; @@ -15,10 +15,10 @@ export default class Editor extends Component { constructor(props) { super(props); const emptyBlock = Block.create({ kind: 'block', type: 'paragraph'}); - const emptyRaw = { nodes: [emptyBlock] }; - const mdast = this.props.value && remarkToSlate(this.props.value); - const mdastHasNodes = !isEmpty(get(mdast, 'nodes')) - const editorState = Raw.deserialize(mdastHasNodes ? mdast : emptyRaw, { terse: true }); + const emptyRawDoc = { nodes: [emptyBlock] }; + const rawDoc = this.props.value && markdownToSlate(this.props.value); + const rawDocHasNodes = !isEmpty(get(rawDoc, 'nodes')) + const editorState = Raw.deserialize(rawDocHasNodes ? rawDoc : emptyRawDoc, { terse: true }); this.state = { editorState, schema: { @@ -46,8 +46,8 @@ export default class Editor extends Component { handleDocumentChange = (doc, editorState) => { const raw = Raw.serialize(editorState, { terse: true }); const plugins = this.state.shortcodePlugins; - const mdast = slateToRemark(raw, plugins); - this.props.onChange(mdast); + const markdown = slateToMarkdown(raw, plugins); + this.props.onChange(markdown); }; hasMark = type => this.state.editorState.marks.some(mark => mark.type === type); @@ -211,5 +211,5 @@ Editor.propTypes = { getAsset: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, onMode: PropTypes.func.isRequired, - value: PropTypes.object, + value: PropTypes.string, }; diff --git a/src/components/Widgets/Markdown/MarkdownControl/index.js b/src/components/Widgets/Markdown/MarkdownControl/index.js index 423bafd8..79b7e866 100644 --- a/src/components/Widgets/Markdown/MarkdownControl/index.js +++ b/src/components/Widgets/Markdown/MarkdownControl/index.js @@ -7,24 +7,13 @@ import { StickyContainer } from '../../../UI/Sticky/Sticky'; const MODE_STORAGE_KEY = 'cms.md-mode'; -/** - * The markdown field value is persisted as a markdown string, but stringifying - * on every keystroke is a big perf hit, so we'll register functions to perform - * those actions only when necessary, such as after loading and before - * persisting. - */ -registry.registerWidgetValueSerializer('markdown', { - serialize: remarkToMarkdown, - deserialize: markdownToRemark, -}); - export default class MarkdownControl extends React.Component { static propTypes = { onChange: PropTypes.func.isRequired, onAddAsset: PropTypes.func.isRequired, onRemoveAsset: PropTypes.func.isRequired, getAsset: PropTypes.func.isRequired, - value: PropTypes.object, + value: PropTypes.string, }; constructor(props) { diff --git a/src/components/Widgets/Markdown/MarkdownPreview/__tests__/renderer.spec.js b/src/components/Widgets/Markdown/MarkdownPreview/__tests__/renderer.spec.js index 02bb94c2..b4d7c0bf 100644 --- a/src/components/Widgets/Markdown/MarkdownPreview/__tests__/renderer.spec.js +++ b/src/components/Widgets/Markdown/MarkdownPreview/__tests__/renderer.spec.js @@ -4,7 +4,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import { padStart } from 'lodash'; import MarkdownPreview from '../index'; -import { markdownToRemark } from '../../serializers'; +import { markdownToHtml } from '../../serializers'; + +const parser = markdownToHtml; describe('Markdown Preview renderer', () => { describe('Markdown rendering', () => { @@ -36,7 +38,7 @@ Text with **bold** & _em_ elements ###### H6 `; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -45,7 +47,7 @@ Text with **bold** & _em_ elements for (const heading of [...Array(6).keys()]) { it(`should render Heading ${ heading + 1 }`, () => { const value = padStart(' Title', heading + 7, '#'); - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); } @@ -64,7 +66,7 @@ Text with **bold** & _em_ elements 1. Sub-Sublist 3 1. ol item 3 `; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -78,7 +80,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] [2]: http://search.yahoo.com/ "Yahoo Search" [3]: http://search.msn.com/ "MSN Search" `; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -86,13 +88,13 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] describe('Code', () => { it('should render code', () => { const value = 'Use the `printf()` function.'; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); it('should render code 2', () => { const value = '``There is a literal backtick (`) here.``'; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -114,7 +116,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]

Test

`; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -123,7 +125,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] describe('HTML rendering', () => { it('should render HTML', () => { const value = '

Paragraph with inline element

'; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); diff --git a/src/components/Widgets/Markdown/MarkdownPreview/index.js b/src/components/Widgets/Markdown/MarkdownPreview/index.js index 461b0b40..c6cbfcf9 100644 --- a/src/components/Widgets/Markdown/MarkdownPreview/index.js +++ b/src/components/Widgets/Markdown/MarkdownPreview/index.js @@ -1,18 +1,18 @@ import React, { PropTypes } from 'react'; -import { remarkToHtml } from '../serializers'; +import { markdownToHtml } from '../serializers'; import previewStyle from '../../defaultPreviewStyle'; const MarkdownPreview = ({ value, getAsset }) => { if (value === null) { return null; } - const html = remarkToHtml(value, getAsset); + const html = markdownToHtml(value, getAsset); return
; }; MarkdownPreview.propTypes = { getAsset: PropTypes.func.isRequired, - value: PropTypes.object, + value: PropTypes.string, }; export default MarkdownPreview; diff --git a/src/components/Widgets/Markdown/serializers/index.js b/src/components/Widgets/Markdown/serializers/index.js index f0d4dbdb..f3c11a64 100644 --- a/src/components/Widgets/Markdown/serializers/index.js +++ b/src/components/Widgets/Markdown/serializers/index.js @@ -12,12 +12,12 @@ import rehypePaperEmoji from './rehypePaperEmoji'; import remarkAssertParents from './remarkAssertParents'; import remarkPaddedLinks from './remarkPaddedLinks'; import remarkWrapHtml from './remarkWrapHtml'; -import remarkToSlatePlugin from './remarkSlate'; +import remarkToSlate from './remarkSlate'; import remarkSquashReferences from './remarkSquashReferences'; import remarkImagesToText from './remarkImagesToText'; import remarkShortcodes from './remarkShortcodes'; import remarkEscapeMarkdownEntities from './remarkEscapeMarkdownEntities' -import slateToRemarkParser from './slateRemark'; +import slateToRemark from './slateRemark'; import registry from '../../../../lib/registry'; /** @@ -35,11 +35,9 @@ import registry from '../../../../lib/registry'; * - MDAST {object} * Also loosely referred to as "Remark". MDAST stands for MarkDown AST * (Abstract Syntax Tree), and is an object representation of a Markdown - * document. Underneath, it's a Unist tree with a Markdown-specific schema. An - * MDAST is used as the source of truth for any Markdown field within the CMS - * once the Markdown string value is loaded. MDAST syntax is a part of the - * Unified ecosystem, and powers the Remark processor, so Remark plugins may - * be used. + * document. Underneath, it's a Unist tree with a Markdown-specific schema. + * MDAST syntax is a part of the Unified ecosystem, and powers the Remark + * processor, so Remark plugins may be used. * * - HAST {object} * Also loosely referred to as "Rehype". HAST, similar to MDAST, is an object @@ -54,55 +52,6 @@ import registry from '../../../../lib/registry'; * Slate's Raw AST is a very simple and unopinionated object representation of * a document in a Slate editor. We define our own Markdown-specific schema * for serialization to/from Slate's Raw AST and MDAST. - * - * Overview of the Markdown widget serialization life cycle: - * - * - Entry Load - * When an entry is loaded, all Markdown widget values are serialized to - * MDAST within the entry draft. - * - * - Visual Editor Render - * When a Markdown widget using the visual editor renders, it converts the - * MDAST value from the entry draft to Slate's Raw AST, and renders that. - * - * - Visual Editor Update - * When the value of a Markdown field is changed in the visual editor, the - * resulting Slate Raw AST is converted back to MDAST, and the MDAST value is - * set as the new state of the field in the entry draft. - * - * - Visual Editor Paste - * When a value is pasted to the visual editor, the pasted value is checked - * for HTML data. If HTML is found, the value is deserialized to an HAST, then - * to MDAST, and finally to Slate's Raw AST. If no HTML is found, the plain - * text value of the paste is serialized to Slate's Raw AST via the Slate - * Plain serializer. The deserialized fragment is then inserted to the Slate - * document. - * - * - Raw Editor Render - * When a Markdown widget using the raw editor (Markdown switch activated), - * it stringifies the MDAST from the entry draft to Markdown, and runs the - * stringified Markdown through Slate's Plain serializer, which outputs a - * Slate Raw AST of the plain text, which is then rendered in the editor. - * - * - Raw Editor Update - * When the value of a Markdown field is changed in the raw editor, the - * resulting Slate Raw AST is stringified back to a string, and the string - * value is then parsed as Markdown into an MDAST. The MDAST value is - * set as the new state of the field in the entry draft. - * - * - Raw Editor Paste - * When a value is pasted to the raw editor, the text value of the paste is - * serialized to Slate's Raw AST via the Slate Plain serializer. The - * deserialized fragment is then inserted to the Slate document. - * - * - Preview Pane Render - * When the preview pane renders the value of a Markdown widget, it first - * converts the MDAST value to HAST, stringifies the HAST to HTML, and - * renders that. - * - * - Entry Persist (Save) - * On persist, the MDAST value in the entry draft is stringified back to - * a Markdown string for storage. */ @@ -180,9 +129,11 @@ export const remarkToMarkdown = obj => { /** - * Convert an MDAST to an HTML string. + * Convert Markdown to HTML. */ -export const remarkToHtml = (mdast, getAsset) => { +export const markdownToHtml = (markdown, getAsset) => { + const mdast = markdownToRemark(markdown); + const hast = unified() .use(remarkToRehypeShortcodes, { plugins: registry.getEditorComponents(), getAsset }) .use(remarkToRehype, { allowDangerousHTML: true }) @@ -216,7 +167,7 @@ export const htmlToSlate = html => { .use(remarkImagesToText) .use(remarkShortcodes, { plugins: registry.getEditorComponents() }) .use(remarkWrapHtml) - .use(remarkToSlatePlugin) + .use(remarkToSlate) .runSync(mdast); return slateRaw; @@ -224,19 +175,22 @@ export const htmlToSlate = html => { /** - * Convert an MDAST to Slate's Raw AST. + * Convert Markdown to Slate's Raw AST. */ -export const remarkToSlate = mdast => { - const result = unified() +export const markdownToSlate = markdown => { + const mdast = markdownToRemark(markdown); + + const slateRaw = unified() .use(remarkWrapHtml) - .use(remarkToSlatePlugin) + .use(remarkToSlate) .runSync(mdast); - return result; + + return slateRaw; }; /** - * Convert a Slate Raw AST to MDAST. + * Convert a Slate Raw AST to Markdown. * * Requires shortcode plugins to parse shortcode nodes back to text. * @@ -244,7 +198,8 @@ export const remarkToSlate = mdast => { * MDAST. The conversion is manual because Unified can only operate on Unist * trees. */ -export const slateToRemark = (raw) => { - const mdast = slateToRemarkParser(raw, { shortcodePlugins: registry.getEditorComponents() }); - return mdast; +export const slateToMarkdown = raw => { + const mdast = slateToRemark(raw, { shortcodePlugins: registry.getEditorComponents() }); + const markdown = remarkToMarkdown(mdast); + return markdown; }; diff --git a/src/components/Widgets/Markdown/serializers/remarkSlate.js b/src/components/Widgets/Markdown/serializers/remarkSlate.js index c7673aa7..f8cc42f0 100644 --- a/src/components/Widgets/Markdown/serializers/remarkSlate.js +++ b/src/components/Widgets/Markdown/serializers/remarkSlate.js @@ -281,7 +281,7 @@ function convertNode(node, nodes) { * A Remark plugin for converting an MDAST to Slate Raw AST. Remark plugins * return a `transform` function that receives the MDAST as it's first argument. */ -export default function remarkToSlatePlugin() { +export default function remarkToSlate() { function transform(node) { /**