Raw text editor (with markdown highlight)
This commit is contained in:
parent
7405ae8f63
commit
994d969247
@ -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"
|
||||
|
@ -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,
|
||||
};
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user