diff --git a/package.json b/package.json index 4ea7e840..b155b71b 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,10 @@ "redux-optimist": "^0.0.2", "redux-thunk": "^1.0.3", "rehype-parse": "^3.1.0", + "rehype-raw": "^1.0.0", + "rehype-react": "^3.0.0", "rehype-remark": "^2.0.0", + "rehype-sanitize": "^2.0.0", "rehype-stringify": "^3.0.0", "remark-html": "^6.0.0", "remark-parse": "^3.0.1", diff --git a/src/components/MarkupItReactRenderer/index.js b/src/components/MarkupItReactRenderer/index.js deleted file mode 100644 index f48f5888..00000000 --- a/src/components/MarkupItReactRenderer/index.js +++ /dev/null @@ -1,85 +0,0 @@ -import React, { PropTypes } from "react"; -import { renderToStaticMarkup } from 'react-dom/server'; -import { Map } from 'immutable'; -import unified from 'unified'; -import markdown from 'remark-parse'; -import rehype from 'remark-rehype'; -import parseHtml from 'rehype-parse'; -import html from 'rehype-stringify'; -import registry from "../../lib/registry"; - -const getPlugins = () => registry.getEditorComponents(); - -const renderEditorPlugins = ({ getAsset }) => { - return tree => { - const result = renderEditorPluginsProcessor(tree, getAsset); - return result; - }; -}; - -const renderEditorPluginsProcessor = (node, getAsset) => { - - if (node.children) { - - node.children = node.children.map(n => renderEditorPluginsProcessor(n, getAsset)); - - // Handle externally defined plugins (they'll be wrapped in paragraphs) - if (node.tagName === 'p' && node.children.length === 1 && node.children[0].type === 'text') { - const value = node.children[0].value; - const plugin = getPlugins().find(plugin => plugin.get('pattern').test(value)); - if (plugin) { - const data = plugin.get('fromBlock')(value.match(plugin.get('pattern'))); - const preview = plugin.get('toPreview')(data); - const output = `
${typeof preview === 'string' ? preview : renderToStaticMarkup(preview)}
`; - return unified().use(parseHtml, { fragment: true }).parse(output); - } - } - } - - // Handle the internally defined image plugin. At this point the token has - // already been parsed as an image by Remark, so we have to catch it by - // checking for the 'image' type. - if (node.tagName === 'img') { - const { src, alt } = node.properties; - - // Until we improve the editor components API for built in components, - // we'll mock the result of String.prototype.match to pass in to the image - // plugin's fromBlock method. - const plugin = getPlugins().get('image'); - if (plugin) { - const matches = [ , alt, src ]; - const data = plugin.get('fromBlock')(matches); - const extendedData = { ...data, image: getAsset(data.image).toString() }; - const preview = plugin.get('toPreview')(extendedData); - const output = typeof preview === 'string' ? -
: - preview; - - const result = unified() - .use(parseHtml, { fragment: true }) - .parse(renderToStaticMarkup(output)); - - return result.children[0]; - } - } - - return node; -}; - -const MarkupItReactRenderer = ({ value, getAsset }) => { - const doc = unified() - .use(markdown, { commonmark: true, footnotes: true, pedantic: true }) - .use(rehype, { allowDangerousHTML: true }) - .use(renderEditorPlugins, { getAsset }) - .use(html, { allowDangerousHTML: true }) - .processSync(value); - - return
; // eslint-disable-line react/no-danger -} - -export default MarkupItReactRenderer; - -MarkupItReactRenderer.propTypes = { - value: PropTypes.string, - getAsset: PropTypes.func.isRequired, -}; diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.css b/src/components/Widgets/MarkdownControl/RawEditor/index.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/RawEditor/index.css rename to src/components/Widgets/MarkdownControl/RawEditor/index.css diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControl/RawEditor/index.js similarity index 97% rename from src/components/Widgets/MarkdownControlElements/RawEditor/index.js rename to src/components/Widgets/MarkdownControl/RawEditor/index.js index 4dba11fe..c83de244 100644 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js +++ b/src/components/Widgets/MarkdownControl/RawEditor/index.js @@ -3,6 +3,8 @@ import unified from 'unified'; import htmlToRehype from 'rehype-parse'; import rehypeToRemark from 'rehype-remark'; import remarkToMarkdown from 'remark-stringify'; +import rehypeSanitize from 'rehype-sanitize'; +import rehypeReparse from 'rehype-raw'; import CaretPosition from 'textarea-caret-position'; import TextareaAutosize from 'react-textarea-autosize'; import registry from '../../../../lib/registry'; @@ -25,9 +27,11 @@ function processUrl(url) { function cleanupPaste(paste) { return unified() - .use(htmlToRehype) + .use(htmlToRehype, { fragment: true }) + .use(rehypeSanitize) + .use(rehypeReparse) .use(rehypeToRemark) - .use(remarkToMarkdown) + .use(remarkToMarkdown, { commonmark: true, footnotes: true, pedantic: true }) .process(paste); } diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.css b/src/components/Widgets/MarkdownControl/Toolbar/Toolbar.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.css rename to src/components/Widgets/MarkdownControl/Toolbar/Toolbar.css diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.js b/src/components/Widgets/MarkdownControl/Toolbar/Toolbar.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/Toolbar.js rename to src/components/Widgets/MarkdownControl/Toolbar/Toolbar.js diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarButton.css b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarButton.css rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.css diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarButton.js b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarButton.js rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarButton.js diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.css b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.css rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.css diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.js b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarComponentsMenu.js rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarComponentsMenu.js diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginForm.css b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginForm.css rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.css diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginForm.js b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginForm.js rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginForm.js diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginFormControl.css b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginFormControl.css rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.css diff --git a/src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginFormControl.js b/src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/Toolbar/ToolbarPluginFormControl.js rename to src/components/Widgets/MarkdownControl/Toolbar/ToolbarPluginFormControl.js diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap b/src/components/Widgets/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap similarity index 100% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap rename to src/components/Widgets/MarkdownControl/VisualEditor/__tests__/__snapshots__/parser.spec.js.snap diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js b/src/components/Widgets/MarkdownControl/VisualEditor/__tests__/parser.spec.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/__tests__/parser.spec.js rename to src/components/Widgets/MarkdownControl/VisualEditor/__tests__/parser.spec.js diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.css b/src/components/Widgets/MarkdownControl/VisualEditor/index.css similarity index 100% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/index.css rename to src/components/Widgets/MarkdownControl/VisualEditor/index.css diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js b/src/components/Widgets/MarkdownControl/VisualEditor/index.js similarity index 96% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/index.js rename to src/components/Widgets/MarkdownControl/VisualEditor/index.js index a00620ca..15265c1d 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/index.js @@ -11,6 +11,9 @@ import { import { keymap } from 'prosemirror-keymap'; import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown'; import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands'; +import unified from 'unified'; +import markdownToRemark from 'remark-parse'; +import remarkToMarkdown from 'remark-stringify'; import registry from '../../../../lib/registry'; import { createAssetProxy } from '../../../../valueObjects/AssetProxy'; import { buildKeymap } from './keymap'; @@ -147,7 +150,11 @@ export default class Editor extends Component { const { serializer } = this.state; const newState = this.view.state.applyAction(action); const md = serializer.serialize(newState.doc); - this.props.onChange(md); + const processedMarkdown = unified() + .use(markdownToRemark) + .use(remarkToMarkdown, { commonmark: true, footnotes: true, pedantic: true }) + .processSync(md); + this.props.onChange(processedMarkdown.contents); this.view.updateState(newState); if (newState.selection !== this.state.selection) { this.handleSelection(newState); diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/keymap.js b/src/components/Widgets/MarkdownControl/VisualEditor/keymap.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/keymap.js rename to src/components/Widgets/MarkdownControl/VisualEditor/keymap.js diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/markdownToProseMirror.js b/src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/markdownToProseMirror.js rename to src/components/Widgets/MarkdownControl/VisualEditor/markdownToProseMirror.js diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js b/src/components/Widgets/MarkdownControl/VisualEditor/parser.js similarity index 86% rename from src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js rename to src/components/Widgets/MarkdownControl/VisualEditor/parser.js index 7ef6e5a8..e06c4d2d 100644 --- a/src/components/Widgets/MarkdownControlElements/VisualEditor/parser.js +++ b/src/components/Widgets/MarkdownControl/VisualEditor/parser.js @@ -1,5 +1,5 @@ import unified from 'unified'; -import markdown from 'remark-parse'; +import remarkToMarkdown from 'remark-parse'; import { Mark } from 'prosemirror-model'; import markdownToProseMirror from './markdownToProseMirror'; @@ -12,7 +12,7 @@ const state = { activeMarks: Mark.none, textsArray: [] }; */ function parser(src) { const result = unified() - .use(markdown, { commonmark: true, footnotes: true, pedantic: true }) + .use(remarkToMarkdown, { commonmark: true, footnotes: true, pedantic: true }) .parse(src); return unified() diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl/index.js similarity index 80% rename from src/components/Widgets/MarkdownControl.js rename to src/components/Widgets/MarkdownControl/index.js index 446890b4..a3c3b0ab 100644 --- a/src/components/Widgets/MarkdownControl.js +++ b/src/components/Widgets/MarkdownControl/index.js @@ -1,9 +1,8 @@ import React, { PropTypes } from 'react'; -import registry from '../../lib/registry'; -import RawEditor from './MarkdownControlElements/RawEditor'; -import VisualEditor from './MarkdownControlElements/VisualEditor'; -import { processEditorPlugins } from './richText'; -import { StickyContainer } from '../UI/Sticky/Sticky'; +import registry from '../../../lib/registry'; +import RawEditor from './RawEditor'; +import VisualEditor from './VisualEditor'; +import { StickyContainer } from '../../UI/Sticky/Sticky'; const MODE_STORAGE_KEY = 'cms.md-mode'; @@ -21,10 +20,6 @@ export default class MarkdownControl extends React.Component { this.state = { mode: localStorage.getItem(MODE_STORAGE_KEY) || 'visual' }; } - componentWillMount() { - processEditorPlugins(registry.getEditorComponents()); - } - handleMode = (mode) => { this.setState({ mode }); localStorage.setItem(MODE_STORAGE_KEY, mode); diff --git a/src/components/Widgets/MarkdownControlElements/plugins.js b/src/components/Widgets/MarkdownControl/plugins.js similarity index 100% rename from src/components/Widgets/MarkdownControlElements/plugins.js rename to src/components/Widgets/MarkdownControl/plugins.js diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js b/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js deleted file mode 100644 index 1ea5e5d3..00000000 --- a/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js +++ /dev/null @@ -1,116 +0,0 @@ -const marks = { - 'blockquote': { - // > ... - pattern: /^>(?:[\t ]*>)*/m, - alias: 'punctuation' - }, - 'code': [ - { - // Prefixed by 4 spaces or 1 tab - pattern: /^(?: {4}|\t).+/m, - alias: 'keyword' - }, - { - // `code` - // ``code`` - pattern: /``.+?``|`[^`\n]+`/, - alias: 'keyword' - } - ], - 'title': [ - { - // title 1 - // ======= - - // title 2 - // ------- - pattern: /\w+.*(?:\r?\n|\r)(?:==+|--+)/, - alias: 'important', - inside: { - punctuation: /==+$|--+$/ - } - }, - { - // # title 1 - // ###### title 6 - pattern: /(^\s*)#+.+/m, - lookbehind: true, - alias: 'important', - inside: { - punctuation: /^#+|#+$/ - } - } - ], - 'hr': { - // *** - // --- - // * * * - // ----------- - pattern: /(^\s*)([*-])([\t ]*\2){2,}(?=\s*$)/m, - lookbehind: true, - alias: 'punctuation' - }, - 'list': { - // * item - // + item - // - item - // 1. item - pattern: /(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m, - lookbehind: true, - alias: 'punctuation' - }, - 'url-reference': { - // [id]: http://example.com "Optional title" - // [id]: http://example.com 'Optional title' - // [id]: http://example.com (Optional title) - // [id]: "Optional title" - pattern: /!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/, - inside: { - 'variable': { - pattern: /^(!?\[)[^\]]+/, - lookbehind: true - }, - 'string': /(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/, - 'punctuation': /^[\[\]!:]|[<>]/ - }, - alias: 'url' - }, - 'bold': { - // **strong** - // __strong__ - - // Allow only one line break - pattern: /(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/, - lookbehind: true, - inside: { - 'punctuation': /^\*\*|^__|\*\*$|__$/ - } - }, - 'italic': { - // *em* - // _em_ - - // Allow only one line break - pattern: /(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/, - lookbehind: true, - inside: { - 'punctuation': /^[*_]|[*_]$/ - } - }, - 'url': { - // [example](http://example.com "Optional title") - // [example] [id] - pattern: /!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/, - inside: { - 'variable': { - pattern: /(!?\[)[^\]]+(?=\]$)/, - lookbehind: true - }, - 'string': { - pattern: /"(?:\\.|[^"\\])*"(?=\)$)/ - } - } - } -}; - -export default marks; diff --git a/src/components/Widgets/MarkdownPreview.js b/src/components/Widgets/MarkdownPreview.js deleted file mode 100644 index 351ea9b9..00000000 --- a/src/components/Widgets/MarkdownPreview.js +++ /dev/null @@ -1,38 +0,0 @@ -import React, { PropTypes } from 'react'; -import { getSyntaxes } from './richText'; -import MarkupItReactRenderer from '../MarkupItReactRenderer/index'; -import previewStyle from './defaultPreviewStyle'; - -const MarkdownPreview = ({ value, getAsset }) => { - if (value == null) { - return null; - } - - const schema = { - 'mediaproxy': ({ token }) => ( // eslint-disable-line - {token.getIn(['data', - ), - }; - - const { markdown } = getSyntaxes(); - return ( -
- -
- ); -}; - -MarkdownPreview.propTypes = { - getAsset: PropTypes.func.isRequired, - value: PropTypes.string, -}; - -export default MarkdownPreview; diff --git a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js b/src/components/Widgets/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js similarity index 78% rename from src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js rename to src/components/Widgets/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js index 5b72258a..e8859a2a 100644 --- a/src/components/MarkupItReactRenderer/__tests__/MarkupItReactRenderer.spec.js +++ b/src/components/Widgets/MarkdownPreview/__tests__/MarkupItReactRenderer.spec.js @@ -3,7 +3,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { padStart } from 'lodash'; -import MarkupItReactRenderer from '../'; +import MarkdownPreview from '../index'; describe('MarkitupReactRenderer', () => { describe('Markdown rendering', () => { @@ -35,7 +35,7 @@ Text with **bold** & _em_ elements ###### H6 `; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -44,7 +44,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(); }); } @@ -55,15 +55,15 @@ Text with **bold** & _em_ elements const value = ` 1. ol item 1 1. ol item 2 - * Sublist 1 - * Sublist 2 - * Sublist 3 - 1. Sub-Sublist 1 - 1. Sub-Sublist 2 - 1. Sub-Sublist 3 + * Sublist 1 + * Sublist 2 + * Sublist 3 + 1. Sub-Sublist 1 + 1. Sub-Sublist 2 + 1. Sub-Sublist 3 1. ol item 3 `; - const component = shallow(); + const component = shallow(); expect(component.html()).toMatchSnapshot(); }); }); @@ -77,7 +77,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(); }); }); @@ -85,13 +85,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(); }); }); @@ -113,7 +113,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(); }); }); @@ -122,7 +122,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/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap b/src/components/Widgets/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap similarity index 100% rename from src/components/MarkupItReactRenderer/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap rename to src/components/Widgets/MarkdownPreview/__tests__/__snapshots__/MarkupItReactRenderer.spec.js.snap diff --git a/src/components/Widgets/MarkdownPreview/cmsPluginRehype.js b/src/components/Widgets/MarkdownPreview/cmsPluginRehype.js new file mode 100644 index 00000000..3efc0700 --- /dev/null +++ b/src/components/Widgets/MarkdownPreview/cmsPluginRehype.js @@ -0,0 +1,59 @@ +import React, { PropTypes } from "react"; +import { renderToStaticMarkup } from 'react-dom/server'; +import { Map } from 'immutable'; +import isString from 'lodash/isString'; +import isEmpty from 'lodash/isEmpty'; +import unified from 'unified'; +import htmlToRehype from 'rehype-parse'; +import registry from "../../../lib/registry"; + +const cmsPluginRehype = ({ getAsset }) => { + + const plugins = registry.getEditorComponents(); + + return transform; + + function transform(node) { + // Handle externally defined plugins (they'll be wrapped in paragraphs) + if (node.tagName === 'p' && node.children.length === 1) { + if (node.children[0].type === 'text') { + const value = node.children[0].value; + const plugin = plugins.find(plugin => plugin.get('pattern').test(value)); + if (plugin) { + const data = plugin.get('fromBlock')(value.match(plugin.get('pattern'))); + const preview = plugin.get('toPreview')(data); + const output = `
${isString(preview) ? preview : renderToStaticMarkup(preview)}
`; + return unified().use(htmlToRehype, { fragment: true }).parse(output).children[0]; + } + } + + // Handle the internally defined image plugin. At this point the token has + // already been parsed as an image by Remark, so we have to catch it by + // checking for the 'image' type. + if (node.children[0].tagName === 'img') { + const { src, alt } = node.children[0].properties; + + // Until we improve the editor components API for built in components, + // we'll mock the result of String.prototype.match to pass in to the image + // plugin's fromBlock method. + const plugin = plugins.get('image'); + if (plugin) { + const matches = [ , alt, src ]; + const data = plugin.get('fromBlock')(matches); + const extendedData = { ...data, image: getAsset(data.image).toString() }; + const preview = plugin.get('toPreview')(extendedData); + const output = `
${isString(preview) ? preview : renderToStaticMarkup(preview)}
`; + return unified().use(htmlToRehype, { fragment: true }).parse(output).children[0]; + } + } + } + + if (!isEmpty(node.children)) { + node.children = node.children.map(childNode => transform(childNode, getAsset)); + } + + return node; + } +}; + +export default cmsPluginRehype; diff --git a/src/components/Widgets/MarkdownPreview/index.js b/src/components/Widgets/MarkdownPreview/index.js new file mode 100644 index 00000000..cfb09370 --- /dev/null +++ b/src/components/Widgets/MarkdownPreview/index.js @@ -0,0 +1,27 @@ +import React, { PropTypes } from 'react'; +import unified from 'unified'; +import markdownToRemark from 'remark-parse'; +import remarkToRehype from 'remark-rehype'; +import htmlToRehype from 'rehype-parse'; +import rehypeToReact from 'rehype-react'; +import cmsPluginToRehype from './cmsPluginRehype'; +import previewStyle from '../defaultPreviewStyle'; + +const MarkdownPreview = ({ value, getAsset }) => { + const Markdown = unified() + .use(markdownToRemark, { commonmark: true, footnotes: true, pedantic: true }) + .use(remarkToRehype, { allowDangerousHTML: true }) + .use(cmsPluginToRehype, { getAsset }) + .use(rehypeToReact, { createElement: React.createElement }) + .processSync(value) + .contents; + + return value === null ? null :
{Markdown}
; +}; + +MarkdownPreview.propTypes = { + getAsset: PropTypes.func.isRequired, + value: PropTypes.string, +}; + +export default MarkdownPreview; diff --git a/src/components/Widgets/richText.js b/src/components/Widgets/richText.js deleted file mode 100644 index f6442449..00000000 --- a/src/components/Widgets/richText.js +++ /dev/null @@ -1,131 +0,0 @@ -/* eslint react/prop-types: 0, react/no-multi-comp: 0 */ -import React from 'react'; -import { List, Map } from 'immutable'; -import MarkupIt from 'markup-it'; -import markdownSyntax from 'markup-it/syntaxes/markdown'; -import htmlSyntax from 'markup-it/syntaxes/html'; -import reInline from 'markup-it/syntaxes/markdown/re/inline'; -import { Icon } from '../UI'; - -/* - * All Rich text widgets (Markdown, for example) should use Slate for text editing and - * MarkupIt to convert between structured formats (Slate JSON, Markdown, HTML, etc.). - * This module Processes and provides Slate nodes and MarkupIt syntaxes augmented with plugins - */ - -let processedPlugins = List([]); - -const nodes = {}; -let augmentedMarkdownSyntax = markdownSyntax; -let augmentedHTMLSyntax = htmlSyntax; - -function processEditorPlugins(plugins) { - // Since the plugin list is immutable, a simple comparisson is enough - // to determine whether we need to process again. - if (plugins === processedPlugins) return; - - plugins.forEach((plugin) => { - const basicRule = MarkupIt.Rule(plugin.id).regExp(plugin.pattern, (state, match) => ( - { data: plugin.fromBlock(match) } - )); - - const markdownRule = basicRule.toText((state, token) => ( - `${ plugin.toBlock(token.getData().toObject()) }\n\n` - )); - - const htmlRule = basicRule.toText((state, token) => ( - plugin.toPreview(token.getData().toObject()) - )); - - const nodeRenderer = (props) => { - const { node, state } = props; - const isFocused = state.selection.hasEdgeIn(node); - const className = isFocused ? 'plugin active' : 'plugin'; - return ( -
-
-
- {plugin.fields.map(field => `${ field.label }: “${ node.data.get(field.name) }”`)} -
-
- ); - }; - - augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(markdownRule); - augmentedHTMLSyntax = augmentedHTMLSyntax.addInlineRules(htmlRule); - nodes[plugin.id] = nodeRenderer; - }); - - processedPlugins = plugins; -} - -function processAssetProxyPlugins(getAsset) { - const assetProxyRule = MarkupIt.Rule('assetproxy').regExp(reInline.link, (state, match) => { - if (match[0].charAt(0) !== '!') { - // Return if this is not an image - return; - } - - const imgData = Map({ - alt: match[1], - src: match[2], - title: match[3], - }).filter(Boolean); - - return { - data: imgData, - }; - }); - const assetProxyMarkdownRule = assetProxyRule.toText((state, token) => { - const data = token.getData(); - const alt = data.get('alt', ''); - const src = data.get('src', ''); - const title = data.get('title', ''); - - if (title) { - return `![${ alt }](${ src } "${ title }")`; - } else { - return `![${ alt }](${ src })`; - } - }); - const assetProxyHTMLRule = assetProxyRule.toText((state, token) => { - const data = token.getData(); - const alt = data.get('alt', ''); - const src = data.get('src', ''); - return `${`; - }); - - nodes.assetproxy = (props) => { - /* eslint react/prop-types: 0 */ - const { node, state } = props; - const isFocused = state.selection.hasEdgeIn(node); - const className = isFocused ? 'active' : null; - const src = node.data.get('src'); - return ( - - ); - }; - augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(assetProxyMarkdownRule); - augmentedHTMLSyntax = augmentedHTMLSyntax.addInlineRules(assetProxyHTMLRule); -} - -function getPlugins() { - return processedPlugins.map(plugin => ({ - id: plugin.id, - icon: plugin.icon, - fields: plugin.fields, - })).toArray(); -} - -function getNodes() { - return nodes; -} - -function getSyntaxes(getAsset) { - if (getAsset) { - processAssetProxyPlugins(getAsset); - } - return { markdown: augmentedMarkdownSyntax, html: augmentedHTMLSyntax }; -} - -export { processEditorPlugins, getNodes, getSyntaxes, getPlugins }; diff --git a/src/components/stories/MarkupItReactRenderer.js b/src/components/stories/MarkupItReactRenderer.js deleted file mode 100644 index a28e6949..00000000 --- a/src/components/stories/MarkupItReactRenderer.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import markdownSyntax from 'markup-it/syntaxes/markdown'; -import htmlSyntax from 'markup-it/syntaxes/html'; -import MarkupItReactRenderer from '../MarkupItReactRenderer'; -import { storiesOf } from '@kadira/storybook'; - -const mdContent = ` -# Title - -* List 1 -* List 2 -`; - -const htmlContent = ` -

Title

-
    -
  1. List item 1
  2. -
  3. List item 2
  4. -
-`; - -function getAsset(path) { - return path; -} - -storiesOf('MarkupItReactRenderer', module) - .add('Markdown', () => ( - - - )).add('HTML', () => ( - - )); diff --git a/src/components/stories/index.js b/src/components/stories/index.js index 1e73d155..c270c754 100644 --- a/src/components/stories/index.js +++ b/src/components/stories/index.js @@ -2,5 +2,4 @@ import './Card'; import './Icon'; import './Toast'; import './FindBar'; -import './MarkupItReactRenderer'; import './ScrollSync'; diff --git a/src/lib/registry.js b/src/lib/registry.js index 8990c96a..6f5254df 100644 --- a/src/lib/registry.js +++ b/src/lib/registry.js @@ -1,5 +1,5 @@ import { Map } from 'immutable'; -import { newEditorPlugin } from '../components/Widgets/MarkdownControlElements/plugins'; +import { newEditorPlugin } from '../components/Widgets/MarkdownControl/plugins'; const _registry = { templates: {},