From 5a4fe3c214688f5b6abb832ba4fdb347c476700f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Wed, 10 Aug 2016 18:59:56 -0300 Subject: [PATCH] changed markdown serializer --- package.json | 4 +- src/components/Widgets/MarkdownControl.js | 86 ++++++++++++------- .../MarkdownControlElements/BlockTypesMenu.js | 2 +- .../MarkdownControlElements/StylesMenu.js | 14 +-- .../MarkdownControlElements/localRenderers.js | 26 +++--- .../Widgets/MarkdownControlElements/syntax.js | 77 +++++++++++++++++ 6 files changed, 155 insertions(+), 54 deletions(-) create mode 100644 src/components/Widgets/MarkdownControlElements/syntax.js diff --git a/package.json b/package.json index 4b84871d..30d972c6 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl.js index 26c576c4..dee24487 100644 --- a/src/components/Widgets/MarkdownControl.js +++ b/src/components/Widgets/MarkdownControl.js @@ -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', - 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() { diff --git a/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.js b/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.js index a05a155e..5337668f 100644 --- a/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.js +++ b/src/components/Widgets/MarkdownControlElements/BlockTypesMenu.js @@ -96,7 +96,7 @@ export default class BlockTypesMenu extends Component { if (this.state.expanded) { return (
- {this.renderBlockTypeButton('horizontal-rule', 'dot-3')} + {this.renderBlockTypeButton('hr', 'dot-3')}
- {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')}
); diff --git a/src/components/Widgets/MarkdownControlElements/localRenderers.js b/src/components/Widgets/MarkdownControlElements/localRenderers.js index 951d1685..c16fb908 100644 --- a/src/components/Widgets/MarkdownControlElements/localRenderers.js +++ b/src/components/Widgets/MarkdownControlElements/localRenderers.js @@ -9,17 +9,17 @@ export const DEFAULT_NODE = 'paragraph'; // Local node renderers. export const NODES = { - 'block-quote': (props) => {props.children}, - 'bulleted-list': props =>
    {props.children}
, - 'heading1': props => {props.children}, - 'heading2': props => {props.children}, - 'heading3': props => {props.children}, - 'heading4': props => {props.children}, - 'heading5': props => {props.children}, - 'heading6': props => {props.children}, - 'list-item': props =>
  • {props.children}
  • , + 'blockquote': (props) => {props.children}, + 'unordered_list': props =>
      {props.children}
    , + 'header_one': props => {props.children}, + 'header_two': props => {props.children}, + 'header_three': props => {props.children}, + 'header_four': props => {props.children}, + 'header_five': props => {props.children}, + 'header_six': props => {props.children}, + 'list_item': props =>
  • {props.children}
  • , 'paragraph': props => {props.children}, - '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', diff --git a/src/components/Widgets/MarkdownControlElements/syntax.js b/src/components/Widgets/MarkdownControlElements/syntax.js new file mode 100644 index 00000000..6a9a6984 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/syntax.js @@ -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); +}