react-json-ide
Version:
A stylish, editor-like, modular, react component for viewing, editing, and debugging javascript object syntax!
1,557 lines (1,364 loc) • 65.2 kB
JavaScript
/** @license react-json-ide v2.6.0
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread";
import React, { Component } from 'react';
import themes from './themes';
import { identical, getType } from './mitsuketa';
import err from './err';
import { format } from './locale';
import defaultLocale from './locale/en';
class JSONInput extends Component {
constructor(props) {
super(props);
this.updateInternalProps = this.updateInternalProps.bind(this);
this.createMarkup = this.createMarkup.bind(this);
this.onClick = this.onClick.bind(this);
this.onBlur = this.onBlur.bind(this);
this.update = this.update.bind(this);
this.getCursorPosition = this.getCursorPosition.bind(this);
this.setCursorPosition = this.setCursorPosition.bind(this);
this.scheduledUpdate = this.scheduledUpdate.bind(this);
this.setUpdateTime = this.setUpdateTime.bind(this);
this.renderLabels = this.renderLabels.bind(this);
this.newSpan = this.newSpan.bind(this);
this.renderErrorMessage = this.renderErrorMessage.bind(this);
this.onScroll = this.onScroll.bind(this);
this.showPlaceholder = this.showPlaceholder.bind(this);
this.tokenize = this.tokenize.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
this.onPaste = this.onPaste.bind(this);
this.stopEvent = this.stopEvent.bind(this);
this.refContent = null;
this.refLabels = null;
this.updateInternalProps();
this.renderCount = 1;
this.state = {
prevPlaceholder: '',
markupText: '',
plainText: '',
json: '',
jsObject: undefined,
lines: false,
error: false
};
if (!this.props.locale) {
console.warn("[react-json-ide - Deprecation Warning] You did not provide a 'locale' prop for your JSON input - This will be required in a future version. English has been set as a default.");
}
}
updateInternalProps() {
let colors = {},
style = {},
theme = themes.dark_vscode_tribute;
if ('theme' in this.props) if (typeof this.props.theme === 'string') if (this.props.theme in themes) theme = themes[this.props.theme];
colors = theme;
if ('colors' in this.props) colors = {
default: 'default' in this.props.colors ? this.props.colors.default : colors.default,
string: 'string' in this.props.colors ? this.props.colors.string : colors.string,
number: 'number' in this.props.colors ? this.props.colors.number : colors.number,
colon: 'colon' in this.props.colors ? this.props.colors.colon : colors.colon,
keys: 'keys' in this.props.colors ? this.props.colors.keys : colors.keys,
keys_whiteSpace: 'keys_whiteSpace' in this.props.colors ? this.props.colors.keys_whiteSpace : colors.keys_whiteSpace,
primitive: 'primitive' in this.props.colors ? this.props.colors.primitive : colors.primitive,
error: 'error' in this.props.colors ? this.props.colors.error : colors.error,
background: 'background' in this.props.colors ? this.props.colors.background : colors.background,
background_warning: 'background_warning' in this.props.colors ? this.props.colors.background_warning : colors.background_warning
};
this.colors = colors;
if ('style' in this.props) style = {
outerBox: 'outerBox' in this.props.style ? this.props.style.outerBox : {},
container: 'container' in this.props.style ? this.props.style.container : {},
warningBox: 'warningBox' in this.props.style ? this.props.style.warningBox : {},
errorMessage: 'errorMessage' in this.props.style ? this.props.style.errorMessage : {},
body: 'body' in this.props.style ? this.props.style.body : {},
labelColumn: 'labelColumn' in this.props.style ? this.props.style.labelColumn : {},
labels: 'labels' in this.props.style ? this.props.style.labels : {},
contentBox: 'contentBox' in this.props.style ? this.props.style.contentBox : {}
};else style = {
outerBox: {},
container: {},
warningBox: {},
errorMessage: {},
body: {},
labelColumn: {},
labels: {},
contentBox: {}
};
this.style = style;
this.confirmGood = 'confirmGood' in this.props ? this.props.confirmGood : true;
const totalHeight = this.props.height || '610px',
totalWidth = this.props.width || '479px';
this.totalHeight = totalHeight;
this.totalWidth = totalWidth;
if (!('onKeyPressUpdate' in this.props) || this.props.onKeyPressUpdate) {
if (!this.timer) this.timer = setInterval(this.scheduledUpdate, 100);
} else if (this.timer) {
clearInterval(this.timer);
this.timer = false;
}
this.updateTime = false;
this.waitAfterKeyPress = 'waitAfterKeyPress' in this.props ? this.props.waitAfterKeyPress : 1000;
this.resetConfiguration = 'reset' in this.props ? this.props.reset : false;
}
render() {
const id = this.props.id,
markupText = this.state.markupText,
error = this.state.error,
colors = this.colors,
style = this.style,
confirmGood = this.confirmGood,
totalHeight = this.totalHeight,
totalWidth = this.totalWidth,
hasError = error ? 'token' in error : false;
this.renderCount++;
return React.createElement("div", {
name: "outer-box",
id: id && id + '-outer-box',
style: _objectSpread({
display: 'block',
overflow: 'none',
height: totalHeight,
width: totalWidth,
margin: 0,
boxSizing: 'border-box',
position: 'relative'
}, style.outerBox)
}, confirmGood ? React.createElement("div", {
style: {
opacity: hasError ? 0 : 1,
height: '30px',
width: '30px',
position: 'absolute',
top: 0,
right: 0,
transform: 'translate(-25%,25%)',
pointerEvents: 'none',
transitionDuration: '0.2s',
transitionTimingFunction: 'cubic-bezier(0, 1, 0.5, 1)'
}
}, React.createElement("svg", {
height: "30px",
width: "30px",
viewBox: "0 0 100 100"
}, React.createElement("path", {
fillRule: "evenodd",
clipRule: "evenodd",
fill: "green",
opacity: "0.85",
d: "M39.363,79L16,55.49l11.347-11.419L39.694,56.49L72.983,23L84,34.085L39.363,79z"
}))) : void 0, React.createElement("div", {
name: "container",
id: id && id + '-container',
style: _objectSpread({
display: 'block',
height: totalHeight,
width: totalWidth,
margin: 0,
boxSizing: 'border-box',
overflow: 'hidden',
fontFamily: 'Roboto, sans-serif'
}, style.container),
onClick: this.onClick
}, React.createElement("div", {
name: "warning-box",
id: id && id + '-warning-box',
style: _objectSpread({
display: 'block',
overflow: 'hidden',
height: hasError ? '60px' : '0px',
width: '100%',
margin: 0,
backgroundColor: colors.background_warning,
transitionDuration: '0.2s',
transitionTimingFunction: 'cubic-bezier(0, 1, 0.5, 1)'
}, style.warningBox),
onClick: this.onClick
}, React.createElement("span", {
style: {
display: 'inline-block',
height: '60px',
width: '60px',
margin: 0,
boxSizing: 'border-box',
overflow: 'hidden',
verticalAlign: 'top',
pointerEvents: 'none'
},
onClick: this.onClick
}, React.createElement("div", {
style: {
position: 'relative',
top: 0,
left: 0,
height: '60px',
width: '60px',
margin: 0,
pointerEvents: 'none'
},
onClick: this.onClick
}, React.createElement("div", {
style: {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
pointerEvents: 'none'
},
onClick: this.onClick
}, React.createElement("svg", {
height: "25px",
width: "25px",
viewBox: "0 0 100 100"
}, React.createElement("path", {
fillRule: "evenodd",
clipRule: "evenodd",
fill: "red",
d: "M73.9,5.75c0.467-0.467,1.067-0.7,1.8-0.7c0.7,0,1.283,0.233,1.75,0.7l16.8,16.8 c0.467,0.5,0.7,1.084,0.7,1.75c0,0.733-0.233,1.334-0.7,1.801L70.35,50l23.9,23.95c0.5,0.467,0.75,1.066,0.75,1.8 c0,0.667-0.25,1.25-0.75,1.75l-16.8,16.75c-0.534,0.467-1.117,0.7-1.75,0.7s-1.233-0.233-1.8-0.7L50,70.351L26.1,94.25 c-0.567,0.467-1.167,0.7-1.8,0.7c-0.667,0-1.283-0.233-1.85-0.7L5.75,77.5C5.25,77,5,76.417,5,75.75c0-0.733,0.25-1.333,0.75-1.8 L29.65,50L5.75,26.101C5.25,25.667,5,25.066,5,24.3c0-0.666,0.25-1.25,0.75-1.75l16.8-16.8c0.467-0.467,1.05-0.7,1.75-0.7 c0.733,0,1.333,0.233,1.8,0.7L50,29.65L73.9,5.75z"
}))))), React.createElement("span", {
style: {
display: 'inline-block',
height: '60px',
width: 'calc(100% - 60px)',
margin: 0,
overflow: 'hidden',
verticalAlign: 'top',
position: 'absolute',
pointerEvents: 'none'
},
onClick: this.onClick
}, this.renderErrorMessage())), React.createElement("div", {
name: "body",
id: id && id + '-body',
style: _objectSpread({
display: 'flex',
overflow: 'none',
height: hasError ? 'calc(100% - 60px)' : '100%',
width: '',
margin: 0,
resize: 'none',
fontFamily: 'Roboto Mono, Monaco, monospace',
fontSize: '11px',
backgroundColor: colors.background,
transitionDuration: '0.2s',
transitionTimingFunction: 'cubic-bezier(0, 1, 0.5, 1)'
}, style.body),
onClick: this.onClick
}, React.createElement("span", {
name: "labels",
id: id && id + '-labels',
ref: ref => this.refLabels = ref,
style: _objectSpread({
display: 'inline-block',
boxSizing: 'border-box',
verticalAlign: 'top',
height: '100%',
width: '44px',
margin: 0,
padding: '5px 0px 5px 10px',
overflow: 'hidden',
color: '#D4D4D4'
}, style.labelColumn),
onClick: this.onClick
}, this.renderLabels()), React.createElement("span", {
id: id,
ref: ref => this.refContent = ref,
contentEditable: true,
style: _objectSpread({
display: 'inline-block',
boxSizing: 'border-box',
verticalAlign: 'top',
height: '100%',
width: '',
flex: 1,
margin: 0,
padding: '5px',
overflowX: 'hidden',
overflowY: 'auto',
wordWrap: 'break-word',
whiteSpace: 'pre-line',
color: '#D4D4D4',
outline: 'none'
}, style.contentBox),
dangerouslySetInnerHTML: this.createMarkup(markupText),
onKeyPress: this.onKeyPress,
onKeyDown: this.onKeyDown,
onClick: this.onClick,
onBlur: this.onBlur,
onScroll: this.onScroll,
onPaste: this.onPaste,
autoComplete: "off",
autoCorrect: "off",
autoCapitalize: "off",
spellCheck: false
}))));
}
renderErrorMessage() {
const locale = this.props.locale || defaultLocale,
error = this.state.error,
style = this.style;
if (!error) return void 0;
return React.createElement("p", {
style: _objectSpread({
color: 'red',
fontSize: '12px',
position: 'absolute',
width: 'calc(100% - 60px)',
height: '60px',
boxSizing: 'border-box',
margin: 0,
padding: 0,
paddingRight: '10px',
overflowWrap: 'break-word',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
}, style.errorMessage)
}, format(locale.format, error));
}
renderLabels() {
const colors = this.colors,
style = this.style,
errorLine = this.state.error ? this.state.error.line : -1,
lines = this.state.lines ? this.state.lines : 1;
let labels = new Array(lines);
for (var i = 0; i < lines - 1; i++) labels[i] = i + 1;
return labels.map(number => {
const color = number !== errorLine ? colors.default : 'red';
return React.createElement("div", {
key: number,
style: _objectSpread({}, style.labels, {
color: color
})
}, number);
});
}
createMarkup(markupText) {
if (markupText === undefined) return {
__html: ''
};
return {
__html: '' + markupText
};
}
newSpan(i, token, depth) {
let colors = this.colors,
type = token.type,
string = token.string;
let color = '';
switch (type) {
case 'string':
case 'number':
case 'primitive':
case 'error':
color = colors[token.type];
break;
case 'key':
if (string === ' ') color = colors.keys_whiteSpace;else color = colors.keys;
break;
case 'symbol':
if (string === ':') color = colors.colon;else color = colors.default;
break;
default:
color = colors.default;
break;
}
if (string.length !== string.replace(/</g, '').replace(/>/g, '').length) string = '<xmp style=display:inline;>' + string + '</xmp>';
return '<span' + ' type="' + type + '"' + ' value="' + string + '"' + ' depth="' + depth + '"' + ' style="color:' + color + '"' + '>' + string + '</span>';
}
getCursorPosition(countBR) {
/**
* Need to deprecate countBR
* It is used to differenciate between good markup render, and aux render when error found
* Adjustments based on coundBR account for usage of <br> instead of <span> for linebreaks to determine acurate cursor position
* Find a way to consolidate render styles
*/
const isChildOf = node => {
while (node !== null) {
if (node === this.refContent) return true;
node = node.parentNode;
}
return false;
};
let selection = window.getSelection(),
charCount = -1,
linebreakCount = 0,
node;
if (selection.focusNode && isChildOf(selection.focusNode)) {
node = selection.focusNode;
charCount = selection.focusOffset;
while (node) {
if (node === this.refContent) break;
if (node.previousSibling) {
node = node.previousSibling;
if (countBR) if (node.nodeName === 'BR') linebreakCount++;
charCount += node.textContent.length;
} else {
node = node.parentNode;
if (node === null) break;
}
}
}
return charCount + linebreakCount;
}
setCursorPosition(nextPosition) {
if ([false, null, undefined].indexOf(nextPosition) > -1) return;
const createRange = (node, chars, range) => {
if (!range) {
range = document.createRange();
range.selectNode(node);
range.setStart(node, 0);
}
if (chars.count === 0) {
range.setEnd(node, chars.count);
} else if (node && chars.count > 0) {
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent.length < chars.count) chars.count -= node.textContent.length;else {
range.setEnd(node, chars.count);
chars.count = 0;
}
} else for (var lp = 0; lp < node.childNodes.length; lp++) {
range = createRange(node.childNodes[lp], chars, range);
if (chars.count === 0) break;
}
}
return range;
};
const setPosition = chars => {
if (chars < 0) return;
let selection = window.getSelection(),
range = createRange(this.refContent, {
count: chars
});
if (!range) return;
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
};
if (nextPosition > 0) setPosition(nextPosition);else this.refContent.focus();
}
update(cursorOffset = 0, updateCursorPosition = true) {
const container = this.refContent,
data = this.tokenize(container);
if ('onChange' in this.props) this.props.onChange({
plainText: data.indented,
markupText: data.markup,
json: data.json,
jsObject: data.jsObject,
lines: data.lines,
error: data.error
});
let cursorPosition = this.getCursorPosition(data.error) + cursorOffset;
this.setState({
plainText: data.indented,
markupText: data.markup,
json: data.json,
jsObject: data.jsObject,
lines: data.lines,
error: data.error
});
this.updateTime = false;
if (updateCursorPosition) this.setCursorPosition(cursorPosition);
}
scheduledUpdate() {
if ('onKeyPressUpdate' in this.props) if (this.props.onKeyPressUpdate === false) return;
const {
updateTime
} = this;
if (updateTime === false) return;
if (updateTime > new Date().getTime()) return;
this.update();
}
setUpdateTime() {
if ('onKeyPressUpdate' in this.props) if (this.props.onKeyPressUpdate === false) return;
this.updateTime = new Date().getTime() + this.waitAfterKeyPress;
}
stopEvent(event) {
if (!event) return;
event.preventDefault();
event.stopPropagation();
}
onKeyPress(event) {
const ctrlOrMetaIsPressed = event.ctrlKey || event.metaKey;
if (this.props.viewOnly && !ctrlOrMetaIsPressed) this.stopEvent(event);
if (!ctrlOrMetaIsPressed) this.setUpdateTime();
}
onKeyDown(event) {
const viewOnly = !!this.props.viewOnly;
const ctrlOrMetaIsPressed = event.ctrlKey || event.metaKey;
switch (event.key) {
case 'Tab':
this.stopEvent(event);
if (viewOnly) break;
document.execCommand("insertText", false, " ");
this.setUpdateTime();
break;
case 'Backspace':
case 'Delete':
if (viewOnly) this.stopEvent(event);
this.setUpdateTime();
break;
case 'ArrowLeft':
case 'ArrowRight':
case 'ArrowUp':
case 'ArrowDown':
this.setUpdateTime();
break;
case 'a':
case 'c':
if (viewOnly && !ctrlOrMetaIsPressed) this.stopEvent(event);
break;
default:
if (viewOnly) this.stopEvent(event);
break;
}
}
onPaste(event) {
if (this.props.viewOnly) {
this.stopEvent(event);
} else {
event.preventDefault();
var text = event.clipboardData.getData('text/plain');
document.execCommand('insertText', false, text);
}
this.update();
}
onClick() {
if ('viewOnly' in this.props) if (this.props.viewOnly) return;
}
onBlur() {
if ('viewOnly' in this.props) if (this.props.viewOnly) return;
this.update(0, false);
}
onScroll(event) {
this.refLabels.scrollTop = event.target.scrollTop;
}
componentDidUpdate() {
this.updateInternalProps();
this.showPlaceholder();
}
componentDidMount() {
this.showPlaceholder();
}
componentWillUnmount() {
if (this.timer) clearInterval(this.timer);
}
showPlaceholder() {
const placeholderDoesNotExist = !('placeholder' in this.props);
if (placeholderDoesNotExist) return;
const {
placeholder
} = this.props;
const placeholderHasEmptyValues = [undefined, null].indexOf(placeholder) > -1;
if (placeholderHasEmptyValues) return;
const {
prevPlaceholder,
jsObject
} = this.state;
const {
resetConfiguration
} = this;
const placeholderDataType = getType(placeholder);
const unexpectedDataType = ['object', 'array'].indexOf(placeholderDataType) === -1;
if (unexpectedDataType) err.throwError('showPlaceholder', 'placeholder', 'either an object or an array');
const samePlaceholderValues = identical(placeholder, prevPlaceholder); // Component will always re-render when new placeholder value is any different from previous placeholder value.
let componentShouldUpdate = !samePlaceholderValues;
if (!componentShouldUpdate) {
if (resetConfiguration) {
/**
* If 'reset' property is set true or is truthy,
* any difference between placeholder and current value
* should trigger component re-render
*/
if (jsObject !== undefined) componentShouldUpdate = !identical(placeholder, jsObject);
}
}
if (!componentShouldUpdate) return;
const data = this.tokenize(placeholder);
this.setState({
prevPlaceholder: placeholder,
plainText: data.indentation,
markupText: data.markup,
lines: data.lines,
error: data.error
});
}
tokenize(something) {
if (typeof something !== 'object') return console.error('tokenize() expects object type properties only. Got \'' + typeof something + '\' type instead.');
const locale = this.props.locale || defaultLocale;
const newSpan = this.newSpan;
/**
* DOM NODE || ONBLUR OR UPDATE
*/
if ('nodeType' in something) {
const containerNode = something.cloneNode(true),
hasChildren = containerNode.hasChildNodes();
if (!hasChildren) return '';
const children = containerNode.childNodes;
let buffer = {
tokens_unknown: [],
tokens_proto: [],
tokens_split: [],
tokens_fallback: [],
tokens_normalize: [],
tokens_merge: [],
tokens_plainText: '',
indented: '',
json: '',
jsObject: undefined,
markup: ''
};
for (var i = 0; i < children.length; i++) {
let child = children[i];
let info = {};
switch (child.nodeName) {
case 'SPAN':
info = {
string: child.textContent,
type: child.attributes.type.textContent
};
buffer.tokens_unknown.push(info);
break;
case 'DIV':
buffer.tokens_unknown.push({
string: child.textContent,
type: 'unknown'
});
break;
case 'BR':
if (child.textContent === '') buffer.tokens_unknown.push({
string: '\n',
type: 'unknown'
});
break;
case '#text':
buffer.tokens_unknown.push({
string: child.wholeText,
type: 'unknown'
});
break;
case 'FONT':
buffer.tokens_unknown.push({
string: child.textContent,
type: 'unknown'
});
break;
default:
console.error('Unrecognized node:', {
child
});
break;
}
}
function quarkize(text, prefix = '') {
let buffer = {
active: false,
string: '',
number: '',
symbol: '',
space: '',
delimiter: '',
quarks: []
};
function pushAndStore(char, type) {
switch (type) {
case 'symbol':
case 'delimiter':
if (buffer.active) buffer.quarks.push({
string: buffer[buffer.active],
type: prefix + '-' + buffer.active
});
buffer[buffer.active] = '';
buffer.active = type;
buffer[buffer.active] = char;
break;
default:
if (type !== buffer.active || [buffer.string, char].indexOf('\n') > -1) {
if (buffer.active) buffer.quarks.push({
string: buffer[buffer.active],
type: prefix + '-' + buffer.active
});
buffer[buffer.active] = '';
buffer.active = type;
buffer[buffer.active] = char;
} else buffer[type] += char;
break;
}
}
function finalPush() {
if (buffer.active) {
buffer.quarks.push({
string: buffer[buffer.active],
type: prefix + '-' + buffer.active
});
buffer[buffer.active] = '';
buffer.active = false;
}
}
for (var i = 0; i < text.length; i++) {
const char = text.charAt(i);
switch (char) {
case '"':
case "'":
pushAndStore(char, 'delimiter');
break;
case ' ':
case '\u00A0':
pushAndStore(char, 'space');
break;
case '{':
case '}':
case '[':
case ']':
case ':':
case ',':
pushAndStore(char, 'symbol');
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (buffer.active === 'string') pushAndStore(char, 'string');else pushAndStore(char, 'number');
break;
case '-':
if (i < text.length - 1) if ('0123456789'.indexOf(text.charAt(i + 1)) > -1) {
pushAndStore(char, 'number');
break;
}
case '.':
if (i < text.length - 1 && i > 0) if ('0123456789'.indexOf(text.charAt(i + 1)) > -1 && '0123456789'.indexOf(text.charAt(i - 1)) > -1) {
pushAndStore(char, 'number');
break;
}
default:
pushAndStore(char, 'string');
break;
}
}
finalPush();
return buffer.quarks;
}
for (var i = 0; i < buffer.tokens_unknown.length; i++) {
let token = buffer.tokens_unknown[i];
buffer.tokens_proto = buffer.tokens_proto.concat(quarkize(token.string, 'proto'));
}
function validToken(string, type) {
const quotes = '\'"';
let firstChar = '',
lastChar = '',
quoteType = false;
switch (type) {
case 'primitive':
if (['true', 'false', 'null', 'undefined'].indexOf(string) === -1) return false;
break;
case 'string':
if (string.length < 2) return false;
firstChar = string.charAt(0), lastChar = string.charAt(string.length - 1), quoteType = quotes.indexOf(firstChar);
if (quoteType === -1) return false;
if (firstChar !== lastChar) return false;
for (var i = 0; i < string.length; i++) {
if (i > 0 && i < string.length - 1) if (string.charAt(i) === quotes[quoteType]) if (string.charAt(i - 1) !== '\\') return false;
}
break;
case 'key':
if (string.length === 0) return false;
firstChar = string.charAt(0), lastChar = string.charAt(string.length - 1), quoteType = quotes.indexOf(firstChar);
if (quoteType > -1) {
if (string.length === 1) return false;
if (firstChar !== lastChar) return false;
for (var i = 0; i < string.length; i++) {
if (i > 0 && i < string.length - 1) if (string.charAt(i) === quotes[quoteType]) if (string.charAt(i - 1) !== '\\') return false;
}
} else {
const nonAlphanumeric = '\'"`.,:;{}[]&<>=~*%\\|/-+!?@^ \xa0';
for (var i = 0; i < nonAlphanumeric.length; i++) {
const nonAlpha = nonAlphanumeric.charAt(i);
if (string.indexOf(nonAlpha) > -1) return false;
}
}
break;
case 'number':
for (var i = 0; i < string.length; i++) {
if ('0123456789'.indexOf(string.charAt(i)) === -1) if (i === 0) {
if ('-' !== string.charAt(0)) return false;
} else if ('.' !== string.charAt(i)) return false;
}
break;
case 'symbol':
if (string.length > 1) return false;
if ('{[:]},'.indexOf(string) === -1) return false;
break;
case 'colon':
if (string.length > 1) return false;
if (':' !== string) return false;
break;
default:
return true;
break;
}
return true;
}
for (var i = 0; i < buffer.tokens_proto.length; i++) {
let token = buffer.tokens_proto[i];
if (token.type.indexOf('proto') === -1) {
if (!validToken(token.string, token.type)) {
buffer.tokens_split = buffer.tokens_split.concat(quarkize(token.string, 'split'));
} else buffer.tokens_split.push(token);
} else buffer.tokens_split.push(token);
}
for (var i = 0; i < buffer.tokens_split.length; i++) {
let token = buffer.tokens_split[i];
let type = token.type,
string = token.string,
length = string.length,
fallback = [];
if (type.indexOf('-') > -1) {
type = type.slice(type.indexOf('-') + 1);
if (type !== 'string') fallback.push('string');
fallback.push('key');
fallback.push('error');
}
let tokul = {
string: string,
length: length,
type: type,
fallback: fallback
};
buffer.tokens_fallback.push(tokul);
}
function tokenFollowed() {
const last = buffer.tokens_normalize.length - 1;
if (last < 1) return false;
for (var i = last; i >= 0; i--) {
const previousToken = buffer.tokens_normalize[i];
switch (previousToken.type) {
case 'space':
case 'linebreak':
break;
default:
return previousToken;
break;
}
}
return false;
}
let buffer2 = {
brackets: [],
stringOpen: false,
isValue: false
};
for (var i = 0; i < buffer.tokens_fallback.length; i++) {
let token = buffer.tokens_fallback[i];
const type = token.type,
string = token.string;
let normalToken = {
type: type,
string: string
};
switch (type) {
case 'symbol':
case 'colon':
if (buffer2.stringOpen) {
if (buffer2.isValue) normalToken.type = 'string';else normalToken.type = 'key';
break;
}
switch (string) {
case '[':
case '{':
buffer2.brackets.push(string);
buffer2.isValue = buffer2.brackets[buffer2.brackets.length - 1] === '[';
break;
case ']':
case '}':
buffer2.brackets.pop();
buffer2.isValue = buffer2.brackets[buffer2.brackets.length - 1] === '[';
break;
case ',':
if (tokenFollowed().type === 'colon') break;
buffer2.isValue = buffer2.brackets[buffer2.brackets.length - 1] === '[';
break;
case ':':
normalToken.type = 'colon';
buffer2.isValue = true;
break;
}
break;
case 'delimiter':
if (buffer2.isValue) normalToken.type = 'string';else normalToken.type = 'key';
if (!buffer2.stringOpen) {
buffer2.stringOpen = string;
break;
}
if (i > 0) {
const previousToken = buffer.tokens_fallback[i - 1],
_string = previousToken.string,
_type = previousToken.type,
_char = _string.charAt(_string.length - 1);
if (_type === 'string' && _char === '\\') break;
}
if (buffer2.stringOpen === string) {
buffer2.stringOpen = false;
break;
}
break;
case 'primitive':
case 'string':
if (['false', 'true', 'null', 'undefined'].indexOf(string) > -1) {
const lastIndex = buffer.tokens_normalize.length - 1;
if (lastIndex >= 0) {
if (buffer.tokens_normalize[lastIndex].type !== 'string') {
normalToken.type = 'primitive';
break;
}
normalToken.type = 'string';
break;
}
normalToken.type = 'primitive';
break;
}
if (string === '\n') if (!buffer2.stringOpen) {
normalToken.type = 'linebreak';
break;
}
if (buffer2.isValue) normalToken.type = 'string';else normalToken.type = 'key';
break;
case 'space':
if (buffer2.stringOpen) if (buffer2.isValue) normalToken.type = 'string';else normalToken.type = 'key';
break;
case 'number':
if (buffer2.stringOpen) if (buffer2.isValue) normalToken.type = 'string';else normalToken.type = 'key';
break;
default:
break;
}
buffer.tokens_normalize.push(normalToken);
}
for (var i = 0; i < buffer.tokens_normalize.length; i++) {
const token = buffer.tokens_normalize[i];
let mergedToken = {
string: token.string,
type: token.type,
tokens: [i]
};
if (['symbol', 'colon'].indexOf(token.type) === -1) if (i + 1 < buffer.tokens_normalize.length) {
let count = 0;
for (var u = i + 1; u < buffer.tokens_normalize.length; u++) {
const nextToken = buffer.tokens_normalize[u];
if (token.type !== nextToken.type) break;
mergedToken.string += nextToken.string;
mergedToken.tokens.push(u);
count++;
}
i += count;
}
buffer.tokens_merge.push(mergedToken);
}
const quotes = '\'"',
alphanumeric = 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + '0123456789' + '_$';
var error = false,
line = buffer.tokens_merge.length > 0 ? 1 : 0;
buffer2 = {
brackets: [],
stringOpen: false,
isValue: false
};
function setError(tokenID, reason, offset = 0) {
error = {
token: tokenID,
line: line,
reason: reason
};
buffer.tokens_merge[tokenID + offset].type = 'error';
}
function followedBySymbol(tokenID, options) {
if (tokenID === undefined) console.error('tokenID argument must be an integer.');
if (options === undefined) console.error('options argument must be an array.');
if (tokenID === buffer.tokens_merge.length - 1) return false;
for (var i = tokenID + 1; i < buffer.tokens_merge.length; i++) {
const nextToken = buffer.tokens_merge[i];
switch (nextToken.type) {
case 'space':
case 'linebreak':
break;
case 'symbol':
case 'colon':
if (options.indexOf(nextToken.string) > -1) return i;else return false;
break;
default:
return false;
break;
}
}
return false;
}
function followsSymbol(tokenID, options) {
if (tokenID === undefined) console.error('tokenID argument must be an integer.');
if (options === undefined) console.error('options argument must be an array.');
if (tokenID === 0) return false;
for (var i = tokenID - 1; i >= 0; i--) {
const previousToken = buffer.tokens_merge[i];
switch (previousToken.type) {
case 'space':
case 'linebreak':
break;
case 'symbol':
case 'colon':
if (options.indexOf(previousToken.string) > -1) return true;
return false;
break;
default:
return false;
break;
}
}
return false;
}
function typeFollowed(tokenID) {
if (tokenID === undefined) console.error('tokenID argument must be an integer.');
if (tokenID === 0) return false;
for (var i = tokenID - 1; i >= 0; i--) {
const previousToken = buffer.tokens_merge[i];
switch (previousToken.type) {
case 'space':
case 'linebreak':
break;
default:
return previousToken.type;
break;
}
}
return false;
}
let bracketList = [];
for (var i = 0; i < buffer.tokens_merge.length; i++) {
if (error) break;
let token = buffer.tokens_merge[i],
string = token.string,
type = token.type,
found = false;
switch (type) {
case 'space':
break;
case 'linebreak':
line++;
break;
case 'symbol':
switch (string) {
case '{':
case '[':
found = followsSymbol(i, ['}', ']']);
if (found) {
setError(i, format(locale.invalidToken.tokenSequence.prohibited, {
firstToken: buffer.tokens_merge[found].string,
secondToken: string
}));
break;
}
if (string === '[' && i > 0) if (!followsSymbol(i, [':', '[', ','])) {
setError(i, format(locale.invalidToken.tokenSequence.permitted, {
firstToken: "[",
secondToken: [":", "[", ","]
}));
break;
}
if (string === '{') if (followsSymbol(i, ['{'])) {
setError(i, format(locale.invalidToken.double, {
token: "{"
}));
break;
}
buffer2.brackets.push(string);
buffer2.isValue = buffer2.brackets[buffer2.brackets.length - 1] === '[';
bracketList.push({
i: i,
line: line,
string: string
});
break;
case '}':
case ']':
if (string === '}') if (buffer2.brackets[buffer2.brackets.length - 1] !== '{') {
setError(i, format(locale.brace.curly.missingOpen));
break;
}
if (string === '}') if (followsSymbol(i, [','])) {
setError(i, format(locale.invalidToken.tokenSequence.prohibited, {
firstToken: ",",
secondToken: "}"
}));
break;
}
if (string === ']') if (buffer2.brackets[buffer2.brackets.length - 1] !== '[') {
setError(i, format(locale.brace.square.missingOpen));
break;
}
if (string === ']') if (followsSymbol(i, [':'])) {
setError(i, format(locale.invalidToken.tokenSequence.prohibited, {
firstToken: ":",
secondToken: "]"
}));
break;
}
buffer2.brackets.pop();
buffer2.isValue = buffer2.brackets[buffer2.brackets.length - 1] === '[';
bracketList.push({
i: i,
line: line,
string: string
});
break;
case ',':
found = followsSymbol(i, ['{']);
if (found) {
if (followedBySymbol(i, ['}'])) {
setError(i, format(locale.brace.curly.cannotWrap, {
token: ","
}));
break;
}
setError(i, format(locale.invalidToken.tokenSequence.prohibited, {
firstToken: "{",
secondToken: ","
}));
break;
}
if (followedBySymbol(i, ['}', ',', ']'])) {
setError(i, format(locale.noTrailingOrLeadingComma));
break;
}
found = typeFollowed(i);
switch (found) {
case 'key':
case 'colon':
setError(i, format(locale.invalidToken.termSequence.prohibited, {
firstTerm: found === 'key' ? locale.types.key : locale.symbols.colon,
secondTerm: locale.symbols.comma
}));
break;
case 'symbol':
if (followsSymbol(i, ['{'])) {
setError(i, format(locale.invalidToken.tokenSequence.prohibited, {
firstToken: "{",
secondToken: ","
}));
break;
}
break;
default:
break;
}
buffer2.isValue = buffer2.brackets[buffer2.brackets.length - 1] === '[';
break;
default:
break;
}
buffer.json += string;
break;
case 'colon':
found = followsSymbol(i, ['[']);
if (found && followedBySymbol(i, [']'])) {
setError(i, format(locale.brace.square.cannotWrap, {
token: ":"
}));
break;
}
if (found) {
setError(i, format(locale.invalidToken.tokenSequence.prohibited, {
firstToken: "[",
secondToken: ":"
}));
break;
}
if (typeFollowed(i) !== 'key') {
setError(i, format(locale.invalidToken.termSequence.permitted, {
firstTerm: locale.symbols.colon,
secondTerm: locale.types.key
}));
break;
}
if (followedBySymbol(i, ['}', ']'])) {
setError(i, format(locale.invalidToken.termSequence.permitted, {
firstTerm: locale.symbols.colon,
secondTerm: locale.types.value
}));
break;
}
buffer2.isValue = true;
buffer.json += string;
break;
case 'key':
case 'string':
let firstChar = string.charAt(0),
lastChar = string.charAt(string.length - 1),
quote_primary = quotes.indexOf(firstChar);
if (quotes.indexOf(firstChar) === -1) if (quotes.indexOf(lastChar) !== -1) {
setError(i, format(locale.string.missingOpen, {
quote: firstChar
}));
break;
}
if (quotes.indexOf(lastChar) === -1) if (quotes.indexOf(firstChar) !== -1) {
setError(i, format(locale.string.missingClose, {
quote: firstChar
}));
break;
}
if (quotes.indexOf(firstChar) > -1) if (firstChar !== lastChar) {
setError(i, format(locale.string.missingClose, {
quote: firstChar
}));
break;
}
if ('string' === type) if (quotes.indexOf(firstChar) === -1 && quotes.indexOf(lastChar) === -1) {
setError(i, format(locale.string.mustBeWrappedByQuotes));
break;
}
if ('key' === type) if (followedBySymbol(i, ['}', ']'])) {
setError(i, format(locale.invalidToken.termSequence.permitted, {
firstTerm: locale.types.key,
secondTerm: locale.symbols.colon
}));
}
if (quotes.indexOf(firstChar) === -1 && quotes.indexOf(lastChar) === -1) for (var h = 0; h < string.length; h++) {
if (error) break;
const c = string.charAt(h);
if (alphanumeric.indexOf(c) === -1) {
setError(i, format(locale.string.nonAlphanumeric, {
token: c
}));
break;
}
}
if (firstChar === "'") string = '"' + string.slice(1, -1) + '"';else if (firstChar !== '"') string = '"' + string + '"';
if ('key' === type) if ('key' === typeFollowed(i)) {
if (i > 0) if (!isNaN(buffer.tokens_merge[i - 1])) {
buffer.tokens_merge[i - 1] += buffer.tokens_merge[i];
setError(i, format(locale.key.numberAndLetterMissingQuotes));
break;
}
setError(i, format(locale.key.spaceMissingQuotes));
break;
}
if ('key' === type) if (!followsSymbol(i, ['{', ','])) {
setError(i, format(locale.invalidToken.tokenSequence.permitted, {
firstToken: type,
secondToken: ["{", ","]
}));
break;
}
if ('string' === type) if (!followsSymbol(i, ['[', ':', ','])) {
setError(i, format(locale.invalidToken.tokenSequence.permitted, {
firstToken: type,
secondToken: ["[", ":", ","]
}));
break;
}
if ('key' === type) if (buffer2.isValue) {
setError(i, format(locale.string.unexpectedKey));
break;
}
if ('string' === type) if (!buffer2.isValue) {
setError(i, format(locale.key.unexpectedString));
break;
}
buffer.json += string;
break;
case 'number':
case 'primitive':
if (followsSymbol(i, ['{'])) {
buffer.tokens_merge[i].type = 'key';
type = buffer.tokens_merge[i].type;
string = '"' + string + '"';
} else if (typeFollowed(i) === 'key') {
buffer.tokens_merge[i].type = 'key';
type = buffer.tokens_merge[i].type;
} else if (!followsSymbol(i, ['[', ':', ','])) {
setError(i, format(locale.invalidToken.tokenSequence.permitted, {
firstToken: type,
secondToken: ["[", ":", ","]
}));
break;
}
if (type !== 'key') if (!buffer2.isValue) {
buffer.tokens_merge[i].type = 'key';
type = buffer.tokens_merge[i].type;
string = '"' + string + '"';
}
if (type === 'primitive') if (string === 'undefined') setError(i, format(locale.invalidToken.useInstead, {
badToken: "undefined",
goodToken: "null"
}));
buffer.json += string;
break;
}
}
let noEscapedSingleQuote = '';
for (var i = 0; i < buffer.json.length; i++) {
let current = buffer.json.charAt(i),
next = '';
if (i + 1 < buffer.json.length) {
next = buffer.json.charAt(i + 1);
if (current === '\\' && next === "'") {
noEscapedSingleQuote += next;
i++;
continue;
}
}
noEscapedSingleQuote += current;
}
buffer.json = noEscapedSingleQuote;
if (!error) {
const maxIterations = Math.ceil(bracketList.length / 2);
let round = 0,
delta = false;
function removePair(index) {
bracketList.splice(index + 1, 1);
bracketList.splice(index, 1);
if (!delta) delta = true;
}
while (bracketList.length > 0) {
delta = false;
for (var tokenCount = 0; tokenCount < bracketList.length - 1; tokenCount++) {
const pair = bracketList[tokenCount].string + bracketList[tokenCount + 1].string;
if (['[]', '{}'].indexOf(pair) > -1) removePair(tokenCount);
}
round++;
if (!delta) break;
if (round >= maxIterations) break;
}
if (bracketList.length > 0) {
const _tokenString = bracketList[0].string,
_tokenPosition = bracketList[0].i,
_closingBracketType = _tokenString === '[' ? ']' : '}';
line = bracketList[0].line;
setError(_tokenPosition, format(locale.brace[_closingBracketType === ']' ? 'square' : 'curly'].missingClose));
}
}
if (!error) if ([undefined, ''].indexOf(buffer.json) === -1) try {
buffer.jsObject = JSON.parse(buffer.json);
} catch (err) {
const errorMessage = err.message,
subsMark = errorMessage.indexOf('position');
if (subsMark === -1) throw new Error('Error parsing failed');
const errPositionStr = errorMessage.substring(subsMark + 9, errorMessage.length),
errPosition = parseInt(errPositionStr);
let charTotal = 0,
tokenIndex = 0,
token = false,
_line = 1,
exitWhile = false;
while (charTotal < errPosition && !exitWhile) {
token = buffer.tokens_merge[tokenIndex];
if ('linebreak' === token.type) _line++;
if (['space', 'linebreak'].indexOf(token.type) === -1) charTotal += token.string.length;
if (charTotal >= errPosition) break;
tokenIndex++;
i