From 994d969247292ddf5a706b76b40de77ee99ffbda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Thu, 11 Aug 2016 17:06:01 -0300 Subject: [PATCH] Raw text editor (with markdown highlight) --- package.json | 1 + .../RawEditor/index.css | 0 .../RawEditor/index.js | 122 ++++++++++++++++++ .../RawEditor/prismMarkdown.js | 116 +++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 src/components/Widgets/MarkdownControlElements/RawEditor/index.css create mode 100644 src/components/Widgets/MarkdownControlElements/RawEditor/index.js create mode 100644 src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js diff --git a/package.json b/package.json index 30d972c6..c91c9083 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.css b/src/components/Widgets/MarkdownControlElements/RawEditor/index.css new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js new file mode 100644 index 00000000..2cabeaa0 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js @@ -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 ( + + ); + } +} + +export default RawEditor; + +RawEditor.propTypes = { + onChange: PropTypes.func.isRequired, + value: PropTypes.node, +}; diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js b/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js new file mode 100644 index 00000000..1ea5e5d3 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js @@ -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]: "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;