Raw text editor (with markdown highlight)

This commit is contained in:
Cássio Zen 2016-08-11 17:06:01 -03:00
parent 7405ae8f63
commit 994d969247
4 changed files with 239 additions and 0 deletions

View File

@ -76,6 +76,7 @@
"lodash": "^4.13.1",
"markup-it": "git+https://github.com/cassiozen/markup-it.git",
"pluralize": "^3.0.0",
"prismjs": "^1.5.1",
"react-portal": "^2.2.1",
"selection-position": "^1.0.0",
"slate": "^0.12.2"

View File

@ -0,0 +1,122 @@
import React, { PropTypes } from 'react';
import { Editor, Plain, Mark } from 'slate';
import Prism from 'prismjs';
import marks from './prismMarkdown';
import styles from './index.css';
const MARKS = {
'highlight-comment': {
opacity: '0.33'
},
'highlight-important': {
fontWeight: 'bold',
color: '#006',
},
'highlight-keyword': {
fontWeight: 'bold',
color: '#006',
},
'highlight-url': {
color: '#006',
},
'highlight-punctuation': {
color: '#006',
}
};
Prism.languages.markdown = Prism.languages.extend('markup', {});
Prism.languages.insertBefore('markdown', 'prolog', marks);
Prism.languages.markdown['bold'].inside['url'] = Prism.util.clone(Prism.languages.markdown['url']);
Prism.languages.markdown['italic'].inside['url'] = Prism.util.clone(Prism.languages.markdown['url']);
Prism.languages.markdown['bold'].inside['italic'] = Prism.util.clone(Prism.languages.markdown['italic']);
Prism.languages.markdown['italic'].inside['bold'] = Prism.util.clone(Prism.languages.markdown['bold']);
class RawEditor extends React.Component {
constructor(props) {
super(props);
const content = props.value ? Plain.deserialize(props.value) : Plain.deserialize('');
this.state = {
state: content
};
this.handleChange = this.handleChange.bind(this);
this.handleDocumentChange = this.handleDocumentChange.bind(this);
this.renderMark = this.renderMark.bind(this);
this.renderDecorations = this.renderDecorations.bind(this);
}
/**
* Slate keeps track of selections, scroll position etc.
* So, onChange gets dispatched on every interaction (click, arrows, everything...)
* It also have an onDocumentChange, that get's dispached only when the actual
* content changes
*/
handleChange(state) {
this.setState({ state });
}
handleDocumentChange(document, state) {
const content = Plain.serialize(state, { terse: true });
this.props.onChange(content);
}
renderMark(mark) {
return MARKS[mark.type] || {};
}
renderDecorations(text, block) {
let characters = text.characters.asMutable();
const string = text.text;
const grammar = Prism.languages.markdown;
const tokens = Prism.tokenize(string, grammar);
let offset = 0;
for (const token of tokens) {
if (typeof token == 'string') {
offset += token.length;
continue;
}
const length = offset + token.matchedStr.length;
const name = token.alias || token.type;
const type = `highlight-${name}`;
for (let i = offset; i < length; i++) {
let char = characters.get(i);
let { marks } = char;
marks = marks.add(Mark.create({ type }));
char = char.merge({ marks });
characters = characters.set(i, char);
}
offset = length;
}
return characters.asImmutable();
}
render() {
return (
<Editor
placeholder={'Enter some rich text...'}
state={this.state.state}
renderMark={this.renderMark}
onChange={this.handleChange}
onDocumentChange={this.handleDocumentChange}
renderDecorations={this.renderDecorations}
/>
);
}
}
export default RawEditor;
RawEditor.propTypes = {
onChange: PropTypes.func.isRequired,
value: PropTypes.node,
};

View File

@ -0,0 +1,116 @@
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;