import React, {Component, PropTypes} from 'react';
import {getDOMNode} from 'react-dom';
import CodeMirror from 'codemirror';
import classnames from 'classnames';
import jq from 'jsonpath';
import vkBeautify from 'vkbeautify';
import {DOMParser} from 'xmldom';
import xpath from 'xpath';
import 'codemirror/mode/css/css';
import 'codemirror/mode/htmlmixed/htmlmixed';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/go/go';
import 'codemirror/mode/shell/shell';
import 'codemirror/mode/clike/clike';
import 'codemirror/mode/mllike/mllike';
import 'codemirror/mode/php/php';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/mode/python/python';
import 'codemirror/mode/ruby/ruby';
import 'codemirror/mode/swift/swift';
import 'codemirror/lib/codemirror.css';
import 'codemirror/addon/dialog/dialog';
import 'codemirror/addon/dialog/dialog.css';
import 'codemirror/addon/fold/foldcode';
import 'codemirror/addon/fold/brace-fold';
import 'codemirror/addon/fold/comment-fold';
import 'codemirror/addon/fold/indent-fold';
import 'codemirror/addon/fold/xml-fold';
import 'codemirror/addon/search/search';
import 'codemirror/addon/search/searchcursor';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/search/matchesonscrollbar';
import 'codemirror/addon/search/matchesonscrollbar.css';
import 'codemirror/addon/fold/foldgutter';
import 'codemirror/addon/fold/foldgutter.css';
import 'codemirror/addon/display/placeholder';
import 'codemirror/addon/lint/lint';
import 'codemirror/addon/lint/json-lint';
import 'codemirror/addon/lint/lint.css';
import 'codemirror/addon/mode/overlay';
import 'codemirror/keymap/vim';
import 'codemirror/keymap/emacs';
import 'codemirror/keymap/sublime';
import '../../css/components/editor.less';
import {showModal} from '../modals/index';
import AlertModal from '../modals/AlertModal';
import Link from '../base/Link';
import * as misc from '../../../common/misc';
import {trackEvent} from '../../../analytics/index';
// Make jsonlint available to the jsonlint plugin
import {parser as jsonlint} from 'jsonlint';
import {prettifyJson} from '../../../common/prettify';
global.jsonlint = jsonlint;
const BASE_CODEMIRROR_OPTIONS = {
lineNumbers: true,
placeholder: 'Start Typing...',
foldGutter: true,
height: 'auto',
lineWrapping: true,
lint: true,
tabSize: 4,
matchBrackets: true,
autoCloseBrackets: true,
indentUnit: 4,
indentWithTabs: true,
keyMap: 'default',
gutters: [
'CodeMirror-linenumbers',
'CodeMirror-foldgutter',
'CodeMirror-lint-markers'
],
cursorScrollMargin: 12, // NOTE: This is px
extraKeys: {
'Ctrl-Q': function (cm) {
cm.foldCode(cm.getCursor());
}
}
};
class Editor extends Component {
constructor (props) {
super(props);
this.state = {
filter: props.filter || ''
};
this._originalCode = '';
}
componentWillUnmount () {
// todo: is there a lighter-weight way to remove the cm instance?
if (this.codeMirror) {
this.codeMirror.toTextArea();
}
}
/**
* Focus the cursor to the editor
*/
focus () {
if (this.codeMirror) {
this.codeMirror.focus();
}
}
selectAll () {
if (this.codeMirror) {
this.codeMirror.setSelection(
{line: 0, ch: 0},
{line: this.codeMirror.lineCount(), ch: 0}
);
}
}
getValue () {
return this.codeMirror.getValue();
}
_handleInitTextarea = textarea => {
if (!textarea) {
// Not mounted
return;
}
if (this.codeMirror) {
// Already initialized
return;
}
const {value} = this.props;
// Add overlay to editor to make all links clickable
CodeMirror.defineMode('master', (config, parserConfig) => {
const baseMode = CodeMirror.getMode(config, parserConfig.baseMode || 'text/plain');
// Only add the click mode if we have links to click
const highlightLinks = !!this.props.onClickLink;
const highlightNunjucks = !this.props.readOnly;
const regexUrl = /^(https?:\/\/)?([\da-z.\-]+)\.([a-z.]{2,6})([\/\w .\-]*)*\/?/;
const regexVariable = /^{{[ |a-zA-Z0-9_\-+,'"\\()\[\]]+}}/;
const regexTag = /^{%[ |a-zA-Z0-9_\-+,'"\\()\[\]]+%}/;
const regexComment = /^{#[^#]+#}/;
const overlay = {
token: function (stream, state) {
if (highlightLinks && stream.match(regexUrl, true)) {
return 'clickable';
}
if (highlightNunjucks && stream.match(regexVariable, true)) {
return 'variable-3';
}
if (highlightNunjucks && stream.match(regexTag, true)) {
return 'variable-3';
}
if (highlightNunjucks && stream.match(regexComment, true)) {
return 'comment';
}
while (stream.next() != null) {
if (stream.match(regexUrl, false)) break;
if (stream.match(regexVariable, false)) break;
if (stream.match(regexTag, false)) break;
if (stream.match(regexComment, false)) break;
}
return null;
}
};
return CodeMirror.overlayMode(baseMode, overlay, true);
});
this.codeMirror = CodeMirror.fromTextArea(textarea, BASE_CODEMIRROR_OPTIONS);
this.codeMirror.on('change', misc.debounce(this._codemirrorValueChanged.bind(this)));
this.codeMirror.on('paste', misc.debounce(this._codemirrorValueChanged.bind(this)));
if (!this.codeMirror.getOption('indentWithTabs')) {
this.codeMirror.setOption('extraKeys', {
Tab: cm => {
const spaces = Array(this.codeMirror.getOption('indentUnit') + 1).join(' ');
cm.replaceSelection(spaces);
}
});
}
// Do this a bit later so we don't block the render process
setTimeout(() => this._codemirrorSetValue(value || ''), 50);
this._codemirrorSetOptions();
};
_handleEditorClick = e => {
if (!this.props.onClickLink) {
return;
}
if (e.target.className.indexOf('cm-clickable') >= 0) {
this.props.onClickLink(e.target.innerHTML);
}
};
_isJSON (mode) {
if (!mode) {
return false;
}
return mode.indexOf('json') !== -1
}
_isXML (mode) {
if (!mode) {
return false;
}
return mode.indexOf('xml') !== -1
}
_handleBeautify () {
trackEvent('Request', 'Beautify');
this._prettify(this.codeMirror.getValue());
}
_prettify (code) {
if (this._isXML(this.props.mode)) {
code = this._prettifyXML(code);
} else {
code = this._prettifyJSON(code);
}
this.codeMirror.setValue(code);
}
_prettifyJSON (code) {
try {
let jsonString = code;
if (this.props.updateFilter && this.state.filter) {
let obj = JSON.parse(code);
try {
jsonString = JSON.stringify(jq.query(obj, this.state.filter));
} catch (err) {
jsonString = '[]';
}
}
return prettifyJson(jsonString, '\t');
} catch (e) {
// That's Ok, just leave it
return code;
}
}
_prettifyXML (code) {
if (this.props.updateFilter && this.state.filter) {
try {
const dom = new DOMParser().parseFromString(code);
const nodes = xpath.select(this.state.filter, dom);
const inner = nodes.map(n => n.toString()).join('\n');
code = `
Use {link} to filter the response body. Here are some examples that you might use on a book store API.
{json ? '$.store.books[*].title' : '/store/books/title'}
|
Get titles of all books in the store |
{json ? '$.store.books[?(@.price < 10)].title' : '/store/books[price < 10]'}
|
Get books costing less than $10 |
{json ? '$.store.books[-1:]' : '/store/books[last()]'}
|
Get the last book in the store |
{json ? '$.store.books.length' : 'count(/store/books)'}
|
Get the number of books in the store |