changed markdown serializer
This commit is contained in:
parent
efddf74404
commit
5a4fe3c214
@ -74,10 +74,10 @@
|
|||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"localforage": "^1.4.2",
|
"localforage": "^1.4.2",
|
||||||
"lodash": "^4.13.1",
|
"lodash": "^4.13.1",
|
||||||
|
"markup-it": "git+https://github.com/cassiozen/markup-it.git",
|
||||||
"pluralize": "^3.0.0",
|
"pluralize": "^3.0.0",
|
||||||
"react-portal": "^2.2.1",
|
"react-portal": "^2.2.1",
|
||||||
"selection-position": "^1.0.0",
|
"selection-position": "^1.0.0",
|
||||||
"slate": "^0.11.2",
|
"slate": "^0.12.2"
|
||||||
"slate-markdown-serializer": "^0.1.5"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Editor, Plain } from 'slate';
|
import { Editor, Raw } from 'slate';
|
||||||
import position from 'selection-position';
|
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 { DEFAULT_NODE, NODES, MARKS } from './MarkdownControlElements/localRenderers';
|
||||||
import StylesMenu from './MarkdownControlElements/StylesMenu';
|
import StylesMenu from './MarkdownControlElements/StylesMenu';
|
||||||
import BlockTypesMenu from './MarkdownControlElements/BlockTypesMenu';
|
import BlockTypesMenu from './MarkdownControlElements/BlockTypesMenu';
|
||||||
@ -15,11 +16,12 @@ class MarkdownControl extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.customMarkdownSerialize = this.customMarkdownSerialize.bind(this);
|
this.getMedia = this.getMedia.bind(this);
|
||||||
this.markdown = new Markdown({ rules: [{ serialize: this.customMarkdownSerialize }] });
|
const MarkdownSyntax = getSyntax(this.getMedia);
|
||||||
|
this.markdown = new MarkupIt(MarkdownSyntax);
|
||||||
|
|
||||||
this.customImageNodeRenderer = this.customImageNodeRenderer.bind(this);
|
this.customImageNodeRenderer = this.customImageNodeRenderer.bind(this);
|
||||||
NODES['image'] = this.customImageNodeRenderer;
|
NODES['mediaproxy'] = this.customImageNodeRenderer;
|
||||||
|
|
||||||
this.blockEdit = false;
|
this.blockEdit = false;
|
||||||
this.menuPositions = {
|
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 = {
|
this.state = {
|
||||||
state: props.value ? this.markdown.deserialize(props.value) : Plain.deserialize('')
|
state: Raw.deserialize(rawJson, { terse: true })
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleChange = this.handleChange.bind(this);
|
this.handleChange = this.handleChange.bind(this);
|
||||||
@ -57,18 +80,13 @@ class MarkdownControl extends React.Component {
|
|||||||
this.renderMark = this.renderMark.bind(this);
|
this.renderMark = this.renderMark.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMedia(src) {
|
||||||
|
return this.props.getMedia(src);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The two custom methods customMarkdownSerialize and customImageNodeRenderer make sure that
|
* Custom local renderer for image proxy.
|
||||||
* both Markdown serializer and Node renderers have access to getMedia with the latest state.
|
|
||||||
*/
|
*/
|
||||||
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) {
|
customImageNodeRenderer(editorProps) {
|
||||||
const { node, state } = editorProps;
|
const { node, state } = editorProps;
|
||||||
const isFocused = state.selection.hasEdgeIn(node);
|
const isFocused = state.selection.hasEdgeIn(node);
|
||||||
@ -95,7 +113,9 @@ class MarkdownControl extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDocumentChange(document, state) {
|
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() {
|
calculateHoverMenuPosition() {
|
||||||
@ -144,13 +164,13 @@ class MarkdownControl extends React.Component {
|
|||||||
const { document } = state;
|
const { document } = state;
|
||||||
|
|
||||||
// Handle everything but list buttons.
|
// Handle everything but list buttons.
|
||||||
if (type != 'bulleted-list' && type != 'numbered-list') {
|
if (type != 'unordered_list' && type != 'ordered_list') {
|
||||||
|
|
||||||
if (isList) {
|
if (isList) {
|
||||||
transform = transform
|
transform = transform
|
||||||
.setBlock(isActive ? DEFAULT_NODE : type)
|
.setBlock(isActive ? DEFAULT_NODE : type)
|
||||||
.unwrapBlock('bulleted-list')
|
.unwrapBlock('unordered_list')
|
||||||
.unwrapBlock('numbered-list');
|
.unwrapBlock('ordered_list');
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
@ -168,14 +188,14 @@ class MarkdownControl extends React.Component {
|
|||||||
if (isList && isType) {
|
if (isList && isType) {
|
||||||
transform = transform
|
transform = transform
|
||||||
.setBlock(DEFAULT_NODE)
|
.setBlock(DEFAULT_NODE)
|
||||||
.unwrapBlock('bulleted-list');
|
.unwrapBlock('unordered_list');
|
||||||
} else if (isList) {
|
} else if (isList) {
|
||||||
transform = transform
|
transform = transform
|
||||||
.unwrapBlock(type == 'bulleted-list')
|
.unwrapBlock(type == 'unordered_list')
|
||||||
.wrapBlock(type);
|
.wrapBlock(type);
|
||||||
} else {
|
} else {
|
||||||
transform = transform
|
transform = transform
|
||||||
.setBlock('list-item')
|
.setBlock('list_item')
|
||||||
.wrapBlock(type);
|
.wrapBlock(type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,16 +257,20 @@ class MarkdownControl extends React.Component {
|
|||||||
handleImageClick(mediaProxy) {
|
handleImageClick(mediaProxy) {
|
||||||
let { state } = this.state;
|
let { state } = this.state;
|
||||||
this.props.onAddMedia(mediaProxy);
|
this.props.onAddMedia(mediaProxy);
|
||||||
state = state
|
|
||||||
.transform()
|
|
||||||
.insertBlock({
|
|
||||||
type: 'image',
|
|
||||||
isVoid: true,
|
|
||||||
data: { src: mediaProxy.path }
|
|
||||||
})
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
this.setState({ state }, this.focusAndAddParagraph);
|
state = state
|
||||||
|
.transform()
|
||||||
|
.insertInline({
|
||||||
|
type: 'mediaproxy',
|
||||||
|
isVoid: true,
|
||||||
|
data: { src: mediaProxy.path }
|
||||||
|
})
|
||||||
|
.collapseToEnd()
|
||||||
|
.insertBlock(DEFAULT_NODE)
|
||||||
|
.focus()
|
||||||
|
.apply();
|
||||||
|
|
||||||
|
this.setState({ state });
|
||||||
}
|
}
|
||||||
|
|
||||||
focusAndAddParagraph() {
|
focusAndAddParagraph() {
|
||||||
|
@ -96,7 +96,7 @@ export default class BlockTypesMenu extends Component {
|
|||||||
if (this.state.expanded) {
|
if (this.state.expanded) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.menu}>
|
<div className={styles.menu}>
|
||||||
{this.renderBlockTypeButton('horizontal-rule', 'dot-3')}
|
{this.renderBlockTypeButton('hr', 'dot-3')}
|
||||||
<Icon type="picture" onClick={this.handleFileUploadClick} className={styles.icon} />
|
<Icon type="picture" onClick={this.handleFileUploadClick} className={styles.icon} />
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
@ -121,14 +121,14 @@ export default class StylesMenu extends Component {
|
|||||||
return (
|
return (
|
||||||
<Portal isOpened={isOpen} onOpen={this.handleOpen}>
|
<Portal isOpened={isOpen} onOpen={this.handleOpen}>
|
||||||
<div className={`${styles.menu} ${styles.hoverMenu}`}>
|
<div className={`${styles.menu} ${styles.hoverMenu}`}>
|
||||||
{this.renderMarkButton('bold', 'bold')}
|
{this.renderMarkButton('BOLD', 'bold')}
|
||||||
{this.renderMarkButton('italic', 'italic')}
|
{this.renderMarkButton('ITALIC', 'italic')}
|
||||||
{this.renderMarkButton('code', 'code')}
|
{this.renderMarkButton('CODE', 'code')}
|
||||||
{this.renderLinkButton()}
|
{this.renderLinkButton()}
|
||||||
{this.renderBlockButton('heading1', 'h1')}
|
{this.renderBlockButton('header_one', 'h1')}
|
||||||
{this.renderBlockButton('heading2', 'h2')}
|
{this.renderBlockButton('header_two', 'h2')}
|
||||||
{this.renderBlockButton('block-quote', 'quote-left')}
|
{this.renderBlockButton('blockquote', 'quote-left')}
|
||||||
{this.renderBlockButton('bulleted-list', 'list-bullet', 'list-item')}
|
{this.renderBlockButton('unordered_list', 'list-bullet', 'list_item')}
|
||||||
</div>
|
</div>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
|
@ -9,17 +9,17 @@ export const DEFAULT_NODE = 'paragraph';
|
|||||||
|
|
||||||
// Local node renderers.
|
// Local node renderers.
|
||||||
export const NODES = {
|
export const NODES = {
|
||||||
'block-quote': (props) => <Block type='blockquote' {...props.attributes}>{props.children}</Block>,
|
'blockquote': (props) => <Block type='blockquote' {...props.attributes}>{props.children}</Block>,
|
||||||
'bulleted-list': props => <Block type='List'><ul {...props.attributes}>{props.children}</ul></Block>,
|
'unordered_list': props => <Block type='List'><ul {...props.attributes}>{props.children}</ul></Block>,
|
||||||
'heading1': props => <Block type='Heading1' {...props.attributes}>{props.children}</Block>,
|
'header_one': props => <Block type='Heading1' {...props.attributes}>{props.children}</Block>,
|
||||||
'heading2': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
'header_two': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||||
'heading3': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
'header_three': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||||
'heading4': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
'header_four': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||||
'heading5': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
'header_five': props => <Block type='Heading2' {...props.attributes}>{props.children}</Block>,
|
||||||
'heading6': 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>,
|
'list_item': props => <li {...props.attributes}>{props.children}</li>,
|
||||||
'paragraph': props => <Block type='Paragraph' {...props.attributes}>{props.children}</Block>,
|
'paragraph': props => <Block type='Paragraph' {...props.attributes}>{props.children}</Block>,
|
||||||
'horizontal-rule': props => {
|
'hr': props => {
|
||||||
const { node, state } = props;
|
const { node, state } = props;
|
||||||
const isFocused = state.selection.hasEdgeIn(node);
|
const isFocused = state.selection.hasEdgeIn(node);
|
||||||
const className = isFocused ? styles.active : null;
|
const className = isFocused ? styles.active : null;
|
||||||
@ -43,13 +43,13 @@ export const NODES = {
|
|||||||
|
|
||||||
// Local mark renderers.
|
// Local mark renderers.
|
||||||
export const MARKS = {
|
export const MARKS = {
|
||||||
bold: {
|
BOLD: {
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
},
|
},
|
||||||
italic: {
|
ITALIC: {
|
||||||
fontStyle: 'italic'
|
fontStyle: 'italic'
|
||||||
},
|
},
|
||||||
code: {
|
CODE: {
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
backgroundColor: '#eee',
|
backgroundColor: '#eee',
|
||||||
padding: '3px',
|
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