changed markdown serializer
This commit is contained in:
parent
efddf74404
commit
5a4fe3c214
@ -74,10 +74,10 @@
|
||||
"json-loader": "^0.5.4",
|
||||
"localforage": "^1.4.2",
|
||||
"lodash": "^4.13.1",
|
||||
"markup-it": "git+https://github.com/cassiozen/markup-it.git",
|
||||
"pluralize": "^3.0.0",
|
||||
"react-portal": "^2.2.1",
|
||||
"selection-position": "^1.0.0",
|
||||
"slate": "^0.11.2",
|
||||
"slate-markdown-serializer": "^0.1.5"
|
||||
"slate": "^0.12.2"
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { Editor, Plain } from 'slate';
|
||||
import { Editor, Raw } from 'slate';
|
||||
import position from 'selection-position';
|
||||
import Markdown from 'slate-markdown-serializer';
|
||||
import MarkupIt, { SlateUtils } from 'markup-it';
|
||||
import getSyntax from './MarkdownControlElements/syntax';
|
||||
import { DEFAULT_NODE, NODES, MARKS } from './MarkdownControlElements/localRenderers';
|
||||
import StylesMenu from './MarkdownControlElements/StylesMenu';
|
||||
import BlockTypesMenu from './MarkdownControlElements/BlockTypesMenu';
|
||||
@ -15,11 +16,12 @@ class MarkdownControl extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.customMarkdownSerialize = this.customMarkdownSerialize.bind(this);
|
||||
this.markdown = new Markdown({ rules: [{ serialize: this.customMarkdownSerialize }] });
|
||||
this.getMedia = this.getMedia.bind(this);
|
||||
const MarkdownSyntax = getSyntax(this.getMedia);
|
||||
this.markdown = new MarkupIt(MarkdownSyntax);
|
||||
|
||||
this.customImageNodeRenderer = this.customImageNodeRenderer.bind(this);
|
||||
NODES['image'] = this.customImageNodeRenderer;
|
||||
NODES['mediaproxy'] = this.customImageNodeRenderer;
|
||||
|
||||
this.blockEdit = false;
|
||||
this.menuPositions = {
|
||||
@ -37,8 +39,29 @@ class MarkdownControl extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
let rawJson;
|
||||
if (props.value !== undefined) {
|
||||
// Parse the markdown
|
||||
const content = this.markdown.toContent(props.value);
|
||||
// Convert the content to JSON
|
||||
rawJson = SlateUtils.encode(content);
|
||||
} else {
|
||||
rawJson = {
|
||||
nodes: [
|
||||
{ kind: 'block',
|
||||
type: 'paragraph',
|
||||
nodes: [{
|
||||
kind: 'text',
|
||||
ranges: [{
|
||||
text: ''
|
||||
}]
|
||||
}]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
this.state = {
|
||||
state: props.value ? this.markdown.deserialize(props.value) : Plain.deserialize('')
|
||||
state: Raw.deserialize(rawJson, { terse: true })
|
||||
};
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
@ -57,18 +80,13 @@ class MarkdownControl extends React.Component {
|
||||
this.renderMark = this.renderMark.bind(this);
|
||||
}
|
||||
|
||||
getMedia(src) {
|
||||
return this.props.getMedia(src);
|
||||
}
|
||||
|
||||
/**
|
||||
* The two custom methods customMarkdownSerialize and customImageNodeRenderer make sure that
|
||||
* both Markdown serializer and Node renderers have access to getMedia with the latest state.
|
||||
* Custom local renderer for image proxy.
|
||||
*/
|
||||
customMarkdownSerialize(obj, children) {
|
||||
if (obj.kind === 'block' && obj.type === 'image') {
|
||||
const src = this.props.getMedia(obj.getIn(['data', 'src']));
|
||||
const alt = obj.getIn(['data', 'alt']) || '';
|
||||
return `![${alt}](${src})`;
|
||||
}
|
||||
}
|
||||
customImageNodeRenderer(editorProps) {
|
||||
const { node, state } = editorProps;
|
||||
const isFocused = state.selection.hasEdgeIn(node);
|
||||
@ -95,7 +113,9 @@ class MarkdownControl extends React.Component {
|
||||
}
|
||||
|
||||
handleDocumentChange(document, state) {
|
||||
this.props.onChange(this.markdown.serialize(state));
|
||||
const rawJson = Raw.serialize(state, { terse: true });
|
||||
const content = SlateUtils.decode(rawJson);
|
||||
this.props.onChange(this.markdown.toText(content));
|
||||
}
|
||||
|
||||
calculateHoverMenuPosition() {
|
||||
@ -144,13 +164,13 @@ class MarkdownControl extends React.Component {
|
||||
const { document } = state;
|
||||
|
||||
// Handle everything but list buttons.
|
||||
if (type != 'bulleted-list' && type != 'numbered-list') {
|
||||
if (type != 'unordered_list' && type != 'ordered_list') {
|
||||
|
||||
if (isList) {
|
||||
transform = transform
|
||||
.setBlock(isActive ? DEFAULT_NODE : type)
|
||||
.unwrapBlock('bulleted-list')
|
||||
.unwrapBlock('numbered-list');
|
||||
.unwrapBlock('unordered_list')
|
||||
.unwrapBlock('ordered_list');
|
||||
}
|
||||
|
||||
else {
|
||||
@ -168,14 +188,14 @@ class MarkdownControl extends React.Component {
|
||||
if (isList && isType) {
|
||||
transform = transform
|
||||
.setBlock(DEFAULT_NODE)
|
||||
.unwrapBlock('bulleted-list');
|
||||
.unwrapBlock('unordered_list');
|
||||
} else if (isList) {
|
||||
transform = transform
|
||||
.unwrapBlock(type == 'bulleted-list')
|
||||
.unwrapBlock(type == 'unordered_list')
|
||||
.wrapBlock(type);
|
||||
} else {
|
||||
transform = transform
|
||||
.setBlock('list-item')
|
||||
.setBlock('list_item')
|
||||
.wrapBlock(type);
|
||||
}
|
||||
}
|
||||
@ -237,16 +257,20 @@ class MarkdownControl extends React.Component {
|
||||
handleImageClick(mediaProxy) {
|
||||
let { state } = this.state;
|
||||
this.props.onAddMedia(mediaProxy);
|
||||
|
||||
state = state
|
||||
.transform()
|
||||
.insertBlock({
|
||||
type: 'image',
|
||||
.insertInline({
|
||||
type: 'mediaproxy',
|
||||
isVoid: true,
|
||||
data: { src: mediaProxy.path }
|
||||
})
|
||||
.collapseToEnd()
|
||||
.insertBlock(DEFAULT_NODE)
|
||||
.focus()
|
||||
.apply();
|
||||
|
||||
this.setState({ state }, this.focusAndAddParagraph);
|
||||
this.setState({ state });
|
||||
}
|
||||
|
||||
focusAndAddParagraph() {
|
||||
|
@ -96,7 +96,7 @@ export default class BlockTypesMenu extends Component {
|
||||
if (this.state.expanded) {
|
||||
return (
|
||||
<div className={styles.menu}>
|
||||
{this.renderBlockTypeButton('horizontal-rule', 'dot-3')}
|
||||
{this.renderBlockTypeButton('hr', 'dot-3')}
|
||||
<Icon type="picture" onClick={this.handleFileUploadClick} className={styles.icon} />
|
||||
<input
|
||||
type="file"
|
||||
|
@ -121,14 +121,14 @@ export default class StylesMenu extends Component {
|
||||
return (
|
||||
<Portal isOpened={isOpen} onOpen={this.handleOpen}>
|
||||
<div className={`${styles.menu} ${styles.hoverMenu}`}>
|
||||
{this.renderMarkButton('bold', 'bold')}
|
||||
{this.renderMarkButton('italic', 'italic')}
|
||||
{this.renderMarkButton('code', 'code')}
|
||||
{this.renderMarkButton('BOLD', 'bold')}
|
||||
{this.renderMarkButton('ITALIC', 'italic')}
|
||||
{this.renderMarkButton('CODE', 'code')}
|
||||
{this.renderLinkButton()}
|
||||
{this.renderBlockButton('heading1', 'h1')}
|
||||
{this.renderBlockButton('heading2', 'h2')}
|
||||
{this.renderBlockButton('block-quote', 'quote-left')}
|
||||
{this.renderBlockButton('bulleted-list', 'list-bullet', 'list-item')}
|
||||
{this.renderBlockButton('header_one', 'h1')}
|
||||
{this.renderBlockButton('header_two', 'h2')}
|
||||
{this.renderBlockButton('blockquote', 'quote-left')}
|
||||
{this.renderBlockButton('unordered_list', 'list-bullet', 'list_item')}
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
|
@ -9,17 +9,17 @@ export const DEFAULT_NODE = 'paragraph';
|
||||
|
||||
// Local node renderers.
|
||||
export const NODES = {
|
||||
'block-quote': (props) => <Block type='blockquote' {...props.attributes}>{props.children}</Block>,
|
||||
'bulleted-list': props => <Block type='List'><ul {...props.attributes}>{props.children}</ul></Block>,
|
||||
'heading1': props => <Block type='Heading1' {...props.attributes}>{props.children}</Block>,
|
||||
'heading2': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||
'heading3': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||
'heading4': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||
'heading5': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||
'heading6': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||
'list-item': props => <li {...props.attributes}>{props.children}</li>,
|
||||
'blockquote': (props) => <Block type='blockquote' {...props.attributes}>{props.children}</Block>,
|
||||
'unordered_list': props => <Block type='List'><ul {...props.attributes}>{props.children}</ul></Block>,
|
||||
'header_one': props => <Block type='Heading1' {...props.attributes}>{props.children}</Block>,
|
||||
'header_two': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||
'header_three': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||
'header_four': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||
'header_five': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||
'header_six': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||
'list_item': props => <li {...props.attributes}>{props.children}</li>,
|
||||
'paragraph': props => <Block type='Paragraph' {...props.attributes}>{props.children}</Block>,
|
||||
'horizontal-rule': props => {
|
||||
'hr': props => {
|
||||
const { node, state } = props;
|
||||
const isFocused = state.selection.hasEdgeIn(node);
|
||||
const className = isFocused ? styles.active : null;
|
||||
@ -43,13 +43,13 @@ export const NODES = {
|
||||
|
||||
// Local mark renderers.
|
||||
export const MARKS = {
|
||||
bold: {
|
||||
BOLD: {
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
italic: {
|
||||
ITALIC: {
|
||||
fontStyle: 'italic'
|
||||
},
|
||||
code: {
|
||||
CODE: {
|
||||
fontFamily: 'monospace',
|
||||
backgroundColor: '#eee',
|
||||
padding: '3px',
|
||||
|
77
src/components/Widgets/MarkdownControlElements/syntax.js
Normal file
77
src/components/Widgets/MarkdownControlElements/syntax.js
Normal file
@ -0,0 +1,77 @@
|
||||
import Immutable from 'immutable';
|
||||
import MarkupIt from 'markup-it';
|
||||
import markdownSyntax from 'markup-it/syntaxes/markdown';
|
||||
import reInline from 'markup-it/syntaxes/markdown/re/inline';
|
||||
|
||||
|
||||
/**
|
||||
* Test if a link input is an image
|
||||
* @param {String} raw
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function isImage(raw) {
|
||||
return raw.charAt(0) === '!';
|
||||
}
|
||||
|
||||
export default function getSyntax(getMedia) {
|
||||
const customImageRule = MarkupIt.Rule('mediaproxy')
|
||||
.regExp(reInline.link, function(state, match) {
|
||||
if (!isImage(match[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
var imgData = Immutable.Map({
|
||||
alt: match[1],
|
||||
src: getMedia(match[2]),
|
||||
title: match[3]
|
||||
}).filter(Boolean);
|
||||
|
||||
return {
|
||||
data: imgData
|
||||
};
|
||||
})
|
||||
.regExp(reInline.reflink, function(state, match) {
|
||||
if (!isImage(match[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
var refId = (match[2] || match[1]);
|
||||
return {
|
||||
data: { ref: refId }
|
||||
};
|
||||
})
|
||||
.regExp(reInline.nolink, function(state, match) {
|
||||
if (!isImage(match[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
var refId = (match[2] || match[1]);
|
||||
return {
|
||||
data: { ref: refId }
|
||||
};
|
||||
})
|
||||
.regExp(reInline.reffn, function(state, match) {
|
||||
if (!isImage(match[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
var refId = match[1];
|
||||
return {
|
||||
data: { ref: refId }
|
||||
};
|
||||
})
|
||||
.toText(function(state, token) {
|
||||
var data = token.getData();
|
||||
var alt = data.get('alt', '');
|
||||
var src = getMedia(data.get('src', ''));
|
||||
var title = data.get('title', '');
|
||||
|
||||
if (title) {
|
||||
return '![' + alt + '](' + src + ' "' + title + '")';
|
||||
} else {
|
||||
return '![' + alt + '](' + src + ')';
|
||||
}
|
||||
});
|
||||
|
||||
return markdownSyntax.addInlineRules(customImageRule);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user