changed markdown serializer

This commit is contained in:
Cássio Zen 2016-08-10 18:59:56 -03:00
parent efddf74404
commit 5a4fe3c214
6 changed files with 155 additions and 54 deletions

View File

@ -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"
}
}

View File

@ -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() {

View File

@ -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"

View 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>
);

View File

@ -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',

View 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);
}