fix visual editor tests, parse/serialize consistency
This commit is contained in:
parent
b22323201d
commit
bd767308cd
@ -158,7 +158,10 @@
|
|||||||
"redux-optimist": "^0.0.2",
|
"redux-optimist": "^0.0.2",
|
||||||
"redux-thunk": "^1.0.3",
|
"redux-thunk": "^1.0.3",
|
||||||
"rehype-parse": "^3.1.0",
|
"rehype-parse": "^3.1.0",
|
||||||
|
"rehype-raw": "^1.0.0",
|
||||||
|
"rehype-react": "^3.0.0",
|
||||||
"rehype-remark": "^2.0.0",
|
"rehype-remark": "^2.0.0",
|
||||||
|
"rehype-sanitize": "^2.0.0",
|
||||||
"rehype-stringify": "^3.0.0",
|
"rehype-stringify": "^3.0.0",
|
||||||
"remark-html": "^6.0.0",
|
"remark-html": "^6.0.0",
|
||||||
"remark-parse": "^3.0.1",
|
"remark-parse": "^3.0.1",
|
||||||
|
@ -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 = `<div>${typeof preview === 'string' ? preview : renderToStaticMarkup(preview)}</div>`;
|
|
||||||
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' ?
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: preview }}/> :
|
|
||||||
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 <div dangerouslySetInnerHTML={{ __html: doc }} />; // eslint-disable-line react/no-danger
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MarkupItReactRenderer;
|
|
||||||
|
|
||||||
MarkupItReactRenderer.propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
getAsset: PropTypes.func.isRequired,
|
|
||||||
};
|
|
@ -3,6 +3,8 @@ import unified from 'unified';
|
|||||||
import htmlToRehype from 'rehype-parse';
|
import htmlToRehype from 'rehype-parse';
|
||||||
import rehypeToRemark from 'rehype-remark';
|
import rehypeToRemark from 'rehype-remark';
|
||||||
import remarkToMarkdown from 'remark-stringify';
|
import remarkToMarkdown from 'remark-stringify';
|
||||||
|
import rehypeSanitize from 'rehype-sanitize';
|
||||||
|
import rehypeReparse from 'rehype-raw';
|
||||||
import CaretPosition from 'textarea-caret-position';
|
import CaretPosition from 'textarea-caret-position';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import registry from '../../../../lib/registry';
|
import registry from '../../../../lib/registry';
|
||||||
@ -25,9 +27,11 @@ function processUrl(url) {
|
|||||||
|
|
||||||
function cleanupPaste(paste) {
|
function cleanupPaste(paste) {
|
||||||
return unified()
|
return unified()
|
||||||
.use(htmlToRehype)
|
.use(htmlToRehype, { fragment: true })
|
||||||
|
.use(rehypeSanitize)
|
||||||
|
.use(rehypeReparse)
|
||||||
.use(rehypeToRemark)
|
.use(rehypeToRemark)
|
||||||
.use(remarkToMarkdown)
|
.use(remarkToMarkdown, { commonmark: true, footnotes: true, pedantic: true })
|
||||||
.process(paste);
|
.process(paste);
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,9 @@ import {
|
|||||||
import { keymap } from 'prosemirror-keymap';
|
import { keymap } from 'prosemirror-keymap';
|
||||||
import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
import { schema as markdownSchema, defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||||
import { baseKeymap, setBlockType, toggleMark } from 'prosemirror-commands';
|
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 registry from '../../../../lib/registry';
|
||||||
import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
|
import { createAssetProxy } from '../../../../valueObjects/AssetProxy';
|
||||||
import { buildKeymap } from './keymap';
|
import { buildKeymap } from './keymap';
|
||||||
@ -147,7 +150,11 @@ export default class Editor extends Component {
|
|||||||
const { serializer } = this.state;
|
const { serializer } = this.state;
|
||||||
const newState = this.view.state.applyAction(action);
|
const newState = this.view.state.applyAction(action);
|
||||||
const md = serializer.serialize(newState.doc);
|
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);
|
this.view.updateState(newState);
|
||||||
if (newState.selection !== this.state.selection) {
|
if (newState.selection !== this.state.selection) {
|
||||||
this.handleSelection(newState);
|
this.handleSelection(newState);
|
@ -1,5 +1,5 @@
|
|||||||
import unified from 'unified';
|
import unified from 'unified';
|
||||||
import markdown from 'remark-parse';
|
import remarkToMarkdown from 'remark-parse';
|
||||||
import { Mark } from 'prosemirror-model';
|
import { Mark } from 'prosemirror-model';
|
||||||
import markdownToProseMirror from './markdownToProseMirror';
|
import markdownToProseMirror from './markdownToProseMirror';
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ const state = { activeMarks: Mark.none, textsArray: [] };
|
|||||||
*/
|
*/
|
||||||
function parser(src) {
|
function parser(src) {
|
||||||
const result = unified()
|
const result = unified()
|
||||||
.use(markdown, { commonmark: true, footnotes: true, pedantic: true })
|
.use(remarkToMarkdown, { commonmark: true, footnotes: true, pedantic: true })
|
||||||
.parse(src);
|
.parse(src);
|
||||||
|
|
||||||
return unified()
|
return unified()
|
@ -1,9 +1,8 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import registry from '../../lib/registry';
|
import registry from '../../../lib/registry';
|
||||||
import RawEditor from './MarkdownControlElements/RawEditor';
|
import RawEditor from './RawEditor';
|
||||||
import VisualEditor from './MarkdownControlElements/VisualEditor';
|
import VisualEditor from './VisualEditor';
|
||||||
import { processEditorPlugins } from './richText';
|
import { StickyContainer } from '../../UI/Sticky/Sticky';
|
||||||
import { StickyContainer } from '../UI/Sticky/Sticky';
|
|
||||||
|
|
||||||
const MODE_STORAGE_KEY = 'cms.md-mode';
|
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' };
|
this.state = { mode: localStorage.getItem(MODE_STORAGE_KEY) || 'visual' };
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
processEditorPlugins(registry.getEditorComponents());
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMode = (mode) => {
|
handleMode = (mode) => {
|
||||||
this.setState({ mode });
|
this.setState({ mode });
|
||||||
localStorage.setItem(MODE_STORAGE_KEY, mode);
|
localStorage.setItem(MODE_STORAGE_KEY, mode);
|
@ -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]: <http://example.com> "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;
|
|
@ -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
|
|
||||||
<img
|
|
||||||
src={getAsset(token.getIn(['data', 'src']))}
|
|
||||||
alt={token.getIn(['data', 'alt'])}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const { markdown } = getSyntaxes();
|
|
||||||
return (
|
|
||||||
<div style={previewStyle}>
|
|
||||||
<MarkupItReactRenderer
|
|
||||||
value={value}
|
|
||||||
syntax={markdown}
|
|
||||||
schema={schema}
|
|
||||||
getAsset={getAsset}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
MarkdownPreview.propTypes = {
|
|
||||||
getAsset: PropTypes.func.isRequired,
|
|
||||||
value: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MarkdownPreview;
|
|
@ -3,7 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { padStart } from 'lodash';
|
import { padStart } from 'lodash';
|
||||||
import MarkupItReactRenderer from '../';
|
import MarkdownPreview from '../index';
|
||||||
|
|
||||||
describe('MarkitupReactRenderer', () => {
|
describe('MarkitupReactRenderer', () => {
|
||||||
describe('Markdown rendering', () => {
|
describe('Markdown rendering', () => {
|
||||||
@ -35,7 +35,7 @@ Text with **bold** & _em_ elements
|
|||||||
|
|
||||||
###### H6
|
###### H6
|
||||||
`;
|
`;
|
||||||
const component = shallow(<MarkupItReactRenderer value={value} />);
|
const component = shallow(<MarkdownPreview value={value} />);
|
||||||
expect(component.html()).toMatchSnapshot();
|
expect(component.html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -44,7 +44,7 @@ Text with **bold** & _em_ elements
|
|||||||
for (const heading of [...Array(6).keys()]) {
|
for (const heading of [...Array(6).keys()]) {
|
||||||
it(`should render Heading ${ heading + 1 }`, () => {
|
it(`should render Heading ${ heading + 1 }`, () => {
|
||||||
const value = padStart(' Title', heading + 7, '#');
|
const value = padStart(' Title', heading + 7, '#');
|
||||||
const component = shallow(<MarkupItReactRenderer value={value} />);
|
const component = shallow(<MarkdownPreview value={value} />);
|
||||||
expect(component.html()).toMatchSnapshot();
|
expect(component.html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -55,15 +55,15 @@ Text with **bold** & _em_ elements
|
|||||||
const value = `
|
const value = `
|
||||||
1. ol item 1
|
1. ol item 1
|
||||||
1. ol item 2
|
1. ol item 2
|
||||||
* Sublist 1
|
* Sublist 1
|
||||||
* Sublist 2
|
* Sublist 2
|
||||||
* Sublist 3
|
* Sublist 3
|
||||||
1. Sub-Sublist 1
|
1. Sub-Sublist 1
|
||||||
1. Sub-Sublist 2
|
1. Sub-Sublist 2
|
||||||
1. Sub-Sublist 3
|
1. Sub-Sublist 3
|
||||||
1. ol item 3
|
1. ol item 3
|
||||||
`;
|
`;
|
||||||
const component = shallow(<MarkupItReactRenderer value={value} />);
|
const component = shallow(<MarkdownPreview value={value} />);
|
||||||
expect(component.html()).toMatchSnapshot();
|
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"
|
[2]: http://search.yahoo.com/ "Yahoo Search"
|
||||||
[3]: http://search.msn.com/ "MSN Search"
|
[3]: http://search.msn.com/ "MSN Search"
|
||||||
`;
|
`;
|
||||||
const component = shallow(<MarkupItReactRenderer value={value} />);
|
const component = shallow(<MarkdownPreview value={value} />);
|
||||||
expect(component.html()).toMatchSnapshot();
|
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', () => {
|
describe('Code', () => {
|
||||||
it('should render code', () => {
|
it('should render code', () => {
|
||||||
const value = 'Use the `printf()` function.';
|
const value = 'Use the `printf()` function.';
|
||||||
const component = shallow(<MarkupItReactRenderer value={value} />);
|
const component = shallow(<MarkdownPreview value={value} />);
|
||||||
expect(component.html()).toMatchSnapshot();
|
expect(component.html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render code 2', () => {
|
it('should render code 2', () => {
|
||||||
const value = '``There is a literal backtick (`) here.``';
|
const value = '``There is a literal backtick (`) here.``';
|
||||||
const component = shallow(<MarkupItReactRenderer value={value} />);
|
const component = shallow(<MarkdownPreview value={value} />);
|
||||||
expect(component.html()).toMatchSnapshot();
|
expect(component.html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -113,7 +113,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
|
|||||||
|
|
||||||
<h1 style="display: block; border: 10px solid #f00; width: 100%">Test</h1>
|
<h1 style="display: block; border: 10px solid #f00; width: 100%">Test</h1>
|
||||||
`;
|
`;
|
||||||
const component = shallow(<MarkupItReactRenderer value={value} />);
|
const component = shallow(<MarkdownPreview value={value} />);
|
||||||
expect(component.html()).toMatchSnapshot();
|
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', () => {
|
describe('HTML rendering', () => {
|
||||||
it('should render HTML', () => {
|
it('should render HTML', () => {
|
||||||
const value = '<p>Paragraph with <em>inline</em> element</p>';
|
const value = '<p>Paragraph with <em>inline</em> element</p>';
|
||||||
const component = shallow(<MarkupItReactRenderer value={value} />);
|
const component = shallow(<MarkdownPreview value={value} />);
|
||||||
expect(component.html()).toMatchSnapshot();
|
expect(component.html()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
59
src/components/Widgets/MarkdownPreview/cmsPluginRehype.js
Normal file
59
src/components/Widgets/MarkdownPreview/cmsPluginRehype.js
Normal file
@ -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 = `<div>${isString(preview) ? preview : renderToStaticMarkup(preview)}</div>`;
|
||||||
|
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 = `<div>${isString(preview) ? preview : renderToStaticMarkup(preview)}</div>`;
|
||||||
|
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;
|
27
src/components/Widgets/MarkdownPreview/index.js
Normal file
27
src/components/Widgets/MarkdownPreview/index.js
Normal file
@ -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 : <div style={previewStyle}>{Markdown}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
MarkdownPreview.propTypes = {
|
||||||
|
getAsset: PropTypes.func.isRequired,
|
||||||
|
value: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MarkdownPreview;
|
@ -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 (
|
|
||||||
<div {...props.attributes} className={className}>
|
|
||||||
<div className="plugin_icon" contentEditable={false}><Icon type={plugin.icon} /></div>
|
|
||||||
<div className="plugin_fields" contentEditable={false}>
|
|
||||||
{plugin.fields.map(field => `${ field.label }: “${ node.data.get(field.name) }”`)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 `<img src=${ getAsset(src) } alt=${ alt } />`;
|
|
||||||
});
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<img {...props.attributes} src={getAsset(src)} className={className} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
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 };
|
|
@ -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 = `
|
|
||||||
<h1>Title</h1>
|
|
||||||
<ol>
|
|
||||||
<li>List item 1</li>
|
|
||||||
<li>List item 2</li>
|
|
||||||
</ol>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function getAsset(path) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
storiesOf('MarkupItReactRenderer', module)
|
|
||||||
.add('Markdown', () => (
|
|
||||||
<MarkupItReactRenderer
|
|
||||||
value={mdContent}
|
|
||||||
syntax={markdownSyntax}
|
|
||||||
getAsset={getAsset}
|
|
||||||
/>
|
|
||||||
|
|
||||||
)).add('HTML', () => (
|
|
||||||
<MarkupItReactRenderer
|
|
||||||
value={htmlContent}
|
|
||||||
syntax={htmlSyntax}
|
|
||||||
getAsset={getAsset}
|
|
||||||
/>
|
|
||||||
));
|
|
@ -2,5 +2,4 @@ import './Card';
|
|||||||
import './Icon';
|
import './Icon';
|
||||||
import './Toast';
|
import './Toast';
|
||||||
import './FindBar';
|
import './FindBar';
|
||||||
import './MarkupItReactRenderer';
|
|
||||||
import './ScrollSync';
|
import './ScrollSync';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Map } from 'immutable';
|
import { Map } from 'immutable';
|
||||||
import { newEditorPlugin } from '../components/Widgets/MarkdownControlElements/plugins';
|
import { newEditorPlugin } from '../components/Widgets/MarkdownControl/plugins';
|
||||||
|
|
||||||
const _registry = {
|
const _registry = {
|
||||||
templates: {},
|
templates: {},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user