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-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",
|
||||
|
@ -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 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);
|
||||
}
|
||||
|
@ -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);
|
@ -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()
|
@ -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);
|
@ -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 { 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(<MarkupItReactRenderer value={value} />);
|
||||
const component = shallow(<MarkdownPreview value={value} />);
|
||||
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(<MarkupItReactRenderer value={value} />);
|
||||
const component = shallow(<MarkdownPreview value={value} />);
|
||||
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(<MarkupItReactRenderer value={value} />);
|
||||
const component = shallow(<MarkdownPreview value={value} />);
|
||||
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(<MarkupItReactRenderer value={value} />);
|
||||
const component = shallow(<MarkdownPreview value={value} />);
|
||||
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(<MarkupItReactRenderer value={value} />);
|
||||
const component = shallow(<MarkdownPreview value={value} />);
|
||||
expect(component.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render code 2', () => {
|
||||
const value = '``There is a literal backtick (`) here.``';
|
||||
const component = shallow(<MarkupItReactRenderer value={value} />);
|
||||
const component = shallow(<MarkdownPreview value={value} />);
|
||||
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>
|
||||
`;
|
||||
const component = shallow(<MarkupItReactRenderer value={value} />);
|
||||
const component = shallow(<MarkdownPreview value={value} />);
|
||||
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 = '<p>Paragraph with <em>inline</em> element</p>';
|
||||
const component = shallow(<MarkupItReactRenderer value={value} />);
|
||||
const component = shallow(<MarkdownPreview value={value} />);
|
||||
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 './Toast';
|
||||
import './FindBar';
|
||||
import './MarkupItReactRenderer';
|
||||
import './ScrollSync';
|
||||
|
@ -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: {},
|
||||
|
Loading…
x
Reference in New Issue
Block a user