UNPKG

jsoneditor

Version:

A web-based tool to view, edit, format, and validate JSON

1,190 lines (1,056 loc) 33.9 kB
'use strict'; require('./polyfills'); var jsonlint = require('./assets/jsonlint/jsonlint'); var jsonMap = require('json-source-map'); var translate = require('./i18n').translate; /** * Parse JSON using the parser built-in in the browser. * On exception, the jsonString is validated and a detailed error is thrown. * @param {String} jsonString * @return {JSON} json */ exports.parse = function parse(jsonString) { try { return JSON.parse(jsonString); } catch (err) { // try to throw a more detailed error message using validate exports.validate(jsonString); // rethrow the original error throw err; } }; /** * Sanitize a JSON-like string containing. For example changes JavaScript * notation into JSON notation. * This function for example changes a string like "{a: 2, 'b': {c: 'd'}" * into '{"a": 2, "b": {"c": "d"}' * @param {string} jsString * @returns {string} json */ exports.sanitize = function (jsString) { // escape all single and double quotes inside strings var chars = []; var i = 0; //If JSON starts with a function (characters/digits/"_-"), remove this function. //This is useful for "stripping" JSONP objects to become JSON //For example: /* some comment */ function_12321321 ( [{"a":"b"}] ); => [{"a":"b"}] var match = jsString.match(/^\s*(\/\*(.|[\r\n])*?\*\/)?\s*[\da-zA-Z_$]+\s*\(([\s\S]*)\)\s*;?\s*$/); if (match) { jsString = match[3]; } var controlChars = { '\b': '\\b', '\f': '\\f', '\n': '\\n', '\r': '\\r', '\t': '\\t' }; var quote = '\''; var quoteDbl = '"'; var quoteLeft = '\u2018'; var quoteRight = '\u2019'; var quoteDblLeft = '\u201C'; var quoteDblRight = '\u201D'; var graveAccent = '\u0060'; var acuteAccent = '\u00B4'; // helper functions to get the current/prev/next character function curr () { return jsString.charAt(i); } function next() { return jsString.charAt(i + 1); } function prev() { return jsString.charAt(i - 1); } function isWhiteSpace(c) { return c === ' ' || c === '\n' || c === '\r' || c === '\t'; } // get the last parsed non-whitespace character function lastNonWhitespace () { var p = chars.length - 1; while (p >= 0) { var pp = chars[p]; if (!isWhiteSpace(pp)) { return pp; } p--; } return ''; } // get at the first next non-white space character function nextNonWhiteSpace() { var iNext = i + 1; while (iNext < jsString.length && isWhiteSpace(jsString[iNext])) { iNext++; } return jsString[iNext]; } // skip a block comment '/* ... */' function skipBlockComment () { i += 2; while (i < jsString.length && (curr() !== '*' || next() !== '/')) { i++; } i += 2; } // skip a comment '// ...' function skipComment () { i += 2; while (i < jsString.length && (curr() !== '\n')) { i++; } } // parse single or double quoted string function parseString(endQuote) { chars.push('"'); i++; var c = curr(); while (i < jsString.length && c !== endQuote) { if (c === '"' && prev() !== '\\') { // unescaped double quote, escape it chars.push('\\"'); } else if (controlChars.hasOwnProperty(c)) { // replace unescaped control characters with escaped ones chars.push(controlChars[c]) } else if (c === '\\') { // remove the escape character when followed by a single quote ', not needed i++; c = curr(); if (c !== '\'') { chars.push('\\'); } chars.push(c); } else { // regular character chars.push(c); } i++; c = curr(); } if (c === endQuote) { chars.push('"'); i++; } } // parse an unquoted key function parseKey() { var specialValues = ['null', 'true', 'false']; var key = ''; var c = curr(); var regexp = /[a-zA-Z_$\d]/; // letter, number, underscore, dollar character while (regexp.test(c)) { key += c; i++; c = curr(); } if (specialValues.indexOf(key) === -1) { chars.push('"' + key + '"'); } else { chars.push(key); } } while(i < jsString.length) { var c = curr(); if (c === '/' && next() === '*') { skipBlockComment(); } else if (c === '/' && next() === '/') { skipComment(); } else if (c === '\u00A0' || (c >= '\u2000' && c <= '\u200A') || c === '\u202F' || c === '\u205F' || c === '\u3000') { // special white spaces (like non breaking space) chars.push(' '); i++ } else if (c === quote) { parseString(quote); } else if (c === quoteDbl) { parseString(quoteDbl); } else if (c === graveAccent) { parseString(acuteAccent); } else if (c === quoteLeft) { parseString(quoteRight); } else if (c === quoteDblLeft) { parseString(quoteDblRight); } else if (c === ',' && [']', '}'].indexOf(nextNonWhiteSpace()) !== -1) { // skip trailing commas i++; } else if (/[a-zA-Z_$]/.test(c) && ['{', ','].indexOf(lastNonWhitespace()) !== -1) { // an unquoted object key (like a in '{a:2}') parseKey(); } else { chars.push(c); i++; } } return chars.join(''); }; /** * Escape unicode characters. * For example input '\u2661' (length 1) will output '\\u2661' (length 5). * @param {string} text * @return {string} */ exports.escapeUnicodeChars = function (text) { // see https://www.wikiwand.com/en/UTF-16 // note: we leave surrogate pairs as two individual chars, // as JSON doesn't interpret them as a single unicode char. return text.replace(/[\u007F-\uFFFF]/g, function(c) { return '\\u'+('0000' + c.charCodeAt(0).toString(16)).slice(-4); }) }; /** * Validate a string containing a JSON object * This method uses JSONLint to validate the String. If JSONLint is not * available, the built-in JSON parser of the browser is used. * @param {String} jsonString String with an (invalid) JSON object * @throws Error */ exports.validate = function validate(jsonString) { if (typeof(jsonlint) != 'undefined') { jsonlint.parse(jsonString); } else { JSON.parse(jsonString); } }; /** * Extend object a with the properties of object b * @param {Object} a * @param {Object} b * @return {Object} a */ exports.extend = function extend(a, b) { for (var prop in b) { if (b.hasOwnProperty(prop)) { a[prop] = b[prop]; } } return a; }; /** * Remove all properties from object a * @param {Object} a * @return {Object} a */ exports.clear = function clear (a) { for (var prop in a) { if (a.hasOwnProperty(prop)) { delete a[prop]; } } return a; }; /** * Get the type of an object * @param {*} object * @return {String} type */ exports.type = function type (object) { if (object === null) { return 'null'; } if (object === undefined) { return 'undefined'; } if ((object instanceof Number) || (typeof object === 'number')) { return 'number'; } if ((object instanceof String) || (typeof object === 'string')) { return 'string'; } if ((object instanceof Boolean) || (typeof object === 'boolean')) { return 'boolean'; } if ((object instanceof RegExp) || (typeof object === 'regexp')) { return 'regexp'; } if (exports.isArray(object)) { return 'array'; } return 'object'; }; /** * Test whether a text contains a url (matches when a string starts * with 'http://*' or 'https://*' and has no whitespace characters) * @param {String} text */ var isUrlRegex = /^https?:\/\/\S+$/; exports.isUrl = function isUrl (text) { return (typeof text == 'string' || text instanceof String) && isUrlRegex.test(text); }; /** * Tes whether given object is an Array * @param {*} obj * @returns {boolean} returns true when obj is an array */ exports.isArray = function (obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; /** * Retrieve the absolute left value of a DOM element * @param {Element} elem A dom element, for example a div * @return {Number} left The absolute left position of this element * in the browser page. */ exports.getAbsoluteLeft = function getAbsoluteLeft(elem) { var rect = elem.getBoundingClientRect(); return rect.left + window.pageXOffset || document.scrollLeft || 0; }; /** * Retrieve the absolute top value of a DOM element * @param {Element} elem A dom element, for example a div * @return {Number} top The absolute top position of this element * in the browser page. */ exports.getAbsoluteTop = function getAbsoluteTop(elem) { var rect = elem.getBoundingClientRect(); return rect.top + window.pageYOffset || document.scrollTop || 0; }; /** * add a className to the given elements style * @param {Element} elem * @param {String} className */ exports.addClassName = function addClassName(elem, className) { var classes = elem.className.split(' '); if (classes.indexOf(className) == -1) { classes.push(className); // add the class to the array elem.className = classes.join(' '); } }; /** * remove all classes from the given elements style * @param {Element} elem */ exports.removeAllClassNames = function removeAllClassNames(elem) { elem.className = ""; }; /** * add a className to the given elements style * @param {Element} elem * @param {String} className */ exports.removeClassName = function removeClassName(elem, className) { var classes = elem.className.split(' '); var index = classes.indexOf(className); if (index != -1) { classes.splice(index, 1); // remove the class from the array elem.className = classes.join(' '); } }; /** * Strip the formatting from the contents of a div * the formatting from the div itself is not stripped, only from its childs. * @param {Element} divElement */ exports.stripFormatting = function stripFormatting(divElement) { var childs = divElement.childNodes; for (var i = 0, iMax = childs.length; i < iMax; i++) { var child = childs[i]; // remove the style if (child.style) { // TODO: test if child.attributes does contain style child.removeAttribute('style'); } // remove all attributes var attributes = child.attributes; if (attributes) { for (var j = attributes.length - 1; j >= 0; j--) { var attribute = attributes[j]; if (attribute.specified === true) { child.removeAttribute(attribute.name); } } } // recursively strip childs exports.stripFormatting(child); } }; /** * Set focus to the end of an editable div * code from Nico Burns * http://stackoverflow.com/users/140293/nico-burns * http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity * @param {Element} contentEditableElement A content editable div */ exports.setEndOfContentEditable = function setEndOfContentEditable(contentEditableElement) { var range, selection; if(document.createRange) { range = document.createRange();//Create a range (a range is a like the selection but invisible) range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start selection = window.getSelection();//get the selection object (allows you to change selection) selection.removeAllRanges();//remove any selections already made selection.addRange(range);//make the range you have just created the visible selection } }; /** * Select all text of a content editable div. * http://stackoverflow.com/a/3806004/1262753 * @param {Element} contentEditableElement A content editable div */ exports.selectContentEditable = function selectContentEditable(contentEditableElement) { if (!contentEditableElement || contentEditableElement.nodeName != 'DIV') { return; } var sel, range; if (window.getSelection && document.createRange) { range = document.createRange(); range.selectNodeContents(contentEditableElement); sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } }; /** * Get text selection * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore * @return {Range | TextRange | null} range */ exports.getSelection = function getSelection() { if (window.getSelection) { var sel = window.getSelection(); if (sel.getRangeAt && sel.rangeCount) { return sel.getRangeAt(0); } } return null; }; /** * Set text selection * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore * @param {Range | TextRange | null} range */ exports.setSelection = function setSelection(range) { if (range) { if (window.getSelection) { var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); } } }; /** * Get selected text range * @return {Object} params object containing parameters: * {Number} startOffset * {Number} endOffset * {Element} container HTML element holding the * selected text element * Returns null if no text selection is found */ exports.getSelectionOffset = function getSelectionOffset() { var range = exports.getSelection(); if (range && 'startOffset' in range && 'endOffset' in range && range.startContainer && (range.startContainer == range.endContainer)) { return { startOffset: range.startOffset, endOffset: range.endOffset, container: range.startContainer.parentNode }; } return null; }; /** * Set selected text range in given element * @param {Object} params An object containing: * {Element} container * {Number} startOffset * {Number} endOffset */ exports.setSelectionOffset = function setSelectionOffset(params) { if (document.createRange && window.getSelection) { var selection = window.getSelection(); if(selection) { var range = document.createRange(); if (!params.container.firstChild) { params.container.appendChild(document.createTextNode('')); } // TODO: do not suppose that the first child of the container is a textnode, // but recursively find the textnodes range.setStart(params.container.firstChild, params.startOffset); range.setEnd(params.container.firstChild, params.endOffset); exports.setSelection(range); } } }; /** * Get the inner text of an HTML element (for example a div element) * @param {Element} element * @param {Object} [buffer] * @return {String} innerText */ exports.getInnerText = function getInnerText(element, buffer) { var first = (buffer == undefined); if (first) { buffer = { 'text': '', 'flush': function () { var text = this.text; this.text = ''; return text; }, 'set': function (text) { this.text = text; } }; } // text node if (element.nodeValue) { return buffer.flush() + element.nodeValue; } // divs or other HTML elements if (element.hasChildNodes()) { var childNodes = element.childNodes; var innerText = ''; for (var i = 0, iMax = childNodes.length; i < iMax; i++) { var child = childNodes[i]; if (child.nodeName == 'DIV' || child.nodeName == 'P') { var prevChild = childNodes[i - 1]; var prevName = prevChild ? prevChild.nodeName : undefined; if (prevName && prevName != 'DIV' && prevName != 'P' && prevName != 'BR') { innerText += '\n'; buffer.flush(); } innerText += exports.getInnerText(child, buffer); buffer.set('\n'); } else if (child.nodeName == 'BR') { innerText += buffer.flush(); buffer.set('\n'); } else { innerText += exports.getInnerText(child, buffer); } } return innerText; } else { if (element.nodeName == 'P' && exports.getInternetExplorerVersion() != -1) { // On Internet Explorer, a <p> with hasChildNodes()==false is // rendered with a new line. Note that a <p> with // hasChildNodes()==true is rendered without a new line // Other browsers always ensure there is a <br> inside the <p>, // and if not, the <p> does not render a new line return buffer.flush(); } } // br or unknown return ''; }; /** * Test whether an element has the provided parent node somewhere up the node tree. * @param {Element} elem * @param {Element} parent * @return {boolean} */ exports.hasParentNode = function (elem, parent) { var e = elem ? elem.parentNode : undefined; while (e) { if (e === parent) { return true; } e = e.parentNode; } return false; } /** * Returns the version of Internet Explorer or a -1 * (indicating the use of another browser). * Source: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx * @return {Number} Internet Explorer version, or -1 in case of an other browser */ exports.getInternetExplorerVersion = function getInternetExplorerVersion() { if (_ieVersion == -1) { var rv = -1; // Return value assumes failure. if (typeof navigator !== 'undefined' && navigator.appName == 'Microsoft Internet Explorer') { var ua = navigator.userAgent; var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); if (re.exec(ua) != null) { rv = parseFloat( RegExp.$1 ); } } _ieVersion = rv; } return _ieVersion; }; /** * Test whether the current browser is Firefox * @returns {boolean} isFirefox */ exports.isFirefox = function isFirefox () { return (typeof navigator !== 'undefined' && navigator.userAgent.indexOf("Firefox") !== -1); }; /** * cached internet explorer version * @type {Number} * @private */ var _ieVersion = -1; /** * Add and event listener. Works for all browsers * @param {Element} element An html element * @param {string} action The action, for example "click", * without the prefix "on" * @param {function} listener The callback function to be executed * @param {boolean} [useCapture] false by default * @return {function} the created event listener */ exports.addEventListener = function addEventListener(element, action, listener, useCapture) { if (element.addEventListener) { if (useCapture === undefined) useCapture = false; if (action === "mousewheel" && exports.isFirefox()) { action = "DOMMouseScroll"; // For Firefox } element.addEventListener(action, listener, useCapture); return listener; } else if (element.attachEvent) { // Old IE browsers var f = function () { return listener.call(element, window.event); }; element.attachEvent("on" + action, f); return f; } }; /** * Remove an event listener from an element * @param {Element} element An html dom element * @param {string} action The name of the event, for example "mousedown" * @param {function} listener The listener function * @param {boolean} [useCapture] false by default */ exports.removeEventListener = function removeEventListener(element, action, listener, useCapture) { if (element.removeEventListener) { if (useCapture === undefined) useCapture = false; if (action === "mousewheel" && exports.isFirefox()) { action = "DOMMouseScroll"; // For Firefox } element.removeEventListener(action, listener, useCapture); } else if (element.detachEvent) { // Old IE browsers element.detachEvent("on" + action, listener); } }; /** * Test if an element is a child of a parent element. * @param {Element} elem * @param {Element} parent * @return {boolean} returns true if elem is a child of the parent */ exports.isChildOf = function (elem, parent) { var e = elem.parentNode; while (e) { if (e === parent) { return true; } e = e.parentNode; } return false; }; /** * Parse a JSON path like '.items[3].name' into an array * @param {string} jsonPath * @return {Array} */ exports.parsePath = function parsePath(jsonPath) { var path = []; var i = 0; function parseProperty () { var prop = '' while (jsonPath[i] !== undefined && /[\w$]/.test(jsonPath[i])) { prop += jsonPath[i]; i++; } if (prop === '') { throw new Error('Invalid JSON path: property name expected at index ' + i); } return prop; } function parseIndex (end) { var name = '' while (jsonPath[i] !== undefined && jsonPath[i] !== end) { name += jsonPath[i]; i++; } if (jsonPath[i] !== end) { throw new Error('Invalid JSON path: unexpected end, character ' + end + ' expected') } return name; } while (jsonPath[i] !== undefined) { if (jsonPath[i] === '.') { i++; path.push(parseProperty()); } else if (jsonPath[i] === '[') { i++; if (jsonPath[i] === '\'' || jsonPath[i] === '"') { var end = jsonPath[i] i++; path.push(parseIndex(end)); if (jsonPath[i] !== end) { throw new Error('Invalid JSON path: closing quote \' expected at index ' + i) } i++; } else { var index = parseIndex(']').trim() if (index.length === 0) { throw new Error('Invalid JSON path: array value expected at index ' + i) } // Coerce numeric indices to numbers, but ignore star index = index === '*' ? index : JSON.parse(index); path.push(index); } if (jsonPath[i] !== ']') { throw new Error('Invalid JSON path: closing bracket ] expected at index ' + i) } i++; } else { throw new Error('Invalid JSON path: unexpected character "' + jsonPath[i] + '" at index ' + i); } } return path; }; /** * Stringify an array with a path in a JSON path like '.items[3].name' * @param {Array.<string | number>} path * @returns {string} */ exports.stringifyPath = function stringifyPath(path) { return path .map(function (p) { if (typeof p === 'number'){ return ('[' + p + ']'); } else if(typeof p === 'string' && p.match(/^[A-Za-z0-9_$]+$/)) { return '.' + p; } else { return '["' + p + '"]'; } }) .join(''); }; /** * Improve the error message of a JSON schema error * @param {Object} error * @return {Object} The error */ exports.improveSchemaError = function (error) { if (error.keyword === 'enum' && Array.isArray(error.schema)) { var enums = error.schema; if (enums) { enums = enums.map(function (value) { return JSON.stringify(value); }); if (enums.length > 5) { var more = ['(' + (enums.length - 5) + ' more...)']; enums = enums.slice(0, 5); enums.push(more); } error.message = 'should be equal to one of: ' + enums.join(', '); } } if (error.keyword === 'additionalProperties') { error.message = 'should NOT have additional property: ' + error.params.additionalProperty; } return error; }; /** * Test whether something is a Promise * @param {*} object * @returns {boolean} Returns true when object is a promise, false otherwise */ exports.isPromise = function (object) { return object && typeof object.then === 'function' && typeof object.catch === 'function'; }; /** * Test whether a custom validation error has the correct structure * @param {*} validationError The error to be checked. * @returns {boolean} Returns true if the structure is ok, false otherwise */ exports.isValidValidationError = function (validationError) { return typeof validationError === 'object' && Array.isArray(validationError.path) && typeof validationError.message === 'string'; }; /** * Test whether the child rect fits completely inside the parent rect. * @param {ClientRect} parent * @param {ClientRect} child * @param {number} margin */ exports.insideRect = function (parent, child, margin) { var _margin = margin !== undefined ? margin : 0; return child.left - _margin >= parent.left && child.right + _margin <= parent.right && child.top - _margin >= parent.top && child.bottom + _margin <= parent.bottom; }; /** * Returns a function, that, as long as it continues to be invoked, will not * be triggered. The function will be called after it stops being called for * N milliseconds. * * Source: https://davidwalsh.name/javascript-debounce-function * * @param {function} func * @param {number} wait Number in milliseconds * @param {boolean} [immediate=false] If `immediate` is passed, trigger the * function on the leading edge, instead * of the trailing. * @return {function} Return the debounced function */ exports.debounce = function debounce(func, wait, immediate) { var timeout; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; }; /** * Determines the difference between two texts. * Can only detect one removed or inserted block of characters. * @param {string} oldText * @param {string} newText * @return {{start: number, end: number}} Returns the start and end * of the changed part in newText. */ exports.textDiff = function textDiff(oldText, newText) { var len = newText.length; var start = 0; var oldEnd = oldText.length; var newEnd = newText.length; while (newText.charAt(start) === oldText.charAt(start) && start < len) { start++; } while (newText.charAt(newEnd - 1) === oldText.charAt(oldEnd - 1) && newEnd > start && oldEnd > 0) { newEnd--; oldEnd--; } return {start: start, end: newEnd}; }; /** * Return an object with the selection range or cursor position (if both have the same value) * Support also old browsers (IE8-) * Source: http://ourcodeworld.com/articles/read/282/how-to-get-the-current-cursor-position-and-selection-within-a-text-input-or-textarea-in-javascript * @param {DOMElement} el A dom element of a textarea or input text. * @return {Object} reference Object with 2 properties (start and end) with the identifier of the location of the cursor and selected text. **/ exports.getInputSelection = function(el) { var startIndex = 0, endIndex = 0, normalizedValue, range, textInputRange, len, endRange; if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") { startIndex = el.selectionStart; endIndex = el.selectionEnd; } else { range = document.selection.createRange(); if (range && range.parentElement() == el) { len = el.value.length; normalizedValue = el.value.replace(/\r\n/g, "\n"); // Create a working TextRange that lives only in the input textInputRange = el.createTextRange(); textInputRange.moveToBookmark(range.getBookmark()); // Check if the startIndex and endIndex of the selection are at the very end // of the input, since moveStart/moveEnd doesn't return what we want // in those cases endRange = el.createTextRange(); endRange.collapse(false); if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { startIndex = endIndex = len; } else { startIndex = -textInputRange.moveStart("character", -len); startIndex += normalizedValue.slice(0, startIndex).split("\n").length - 1; if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) { endIndex = len; } else { endIndex = -textInputRange.moveEnd("character", -len); endIndex += normalizedValue.slice(0, endIndex).split("\n").length - 1; } } } } return { startIndex: startIndex, endIndex: endIndex, start: _positionForIndex(startIndex), end: _positionForIndex(endIndex) }; /** * Returns textarea row and column position for certain index * @param {Number} index text index * @returns {{row: Number, col: Number}} */ function _positionForIndex(index) { var textTillIndex = el.value.substring(0,index); var row = (textTillIndex.match(/\n/g) || []).length + 1; var col = textTillIndex.length - textTillIndex.lastIndexOf("\n"); return { row: row, column: col } } } /** * Returns the index for certaion position in text element * @param {DOMElement} el A dom element of a textarea or input text. * @param {Number} row row value, > 0, if exceeds rows number - last row will be returned * @param {Number} column column value, > 0, if exceeds column length - end of column will be returned * @returns {Number} index of position in text, -1 if not found */ exports.getIndexForPosition = function(el, row, column) { var text = el.value || ''; if (row > 0 && column > 0) { var rows = text.split('\n', row); row = Math.min(rows.length, row); column = Math.min(rows[row - 1].length, column - 1); var columnCount = (row == 1 ? column : column + 1); // count new line on multiple rows return rows.slice(0, row - 1).join('\n').length + columnCount; } return -1; } /** * Returns location of json paths in certain json string * @param {String} text json string * @param {Array<String>} paths array of json paths * @returns {Array<{path: String, line: Number, row: Number}>} */ exports.getPositionForPath = function(text, paths) { var me = this; var result = []; var jsmap; if (!paths || !paths.length) { return result; } try { jsmap = jsonMap.parse(text); } catch (err) { return result; } paths.forEach(function (path) { var pathArr = me.parsePath(path); var pointerName = pathArr.length ? "/" + pathArr.join("/") : ""; var pointer = jsmap.pointers[pointerName]; if (pointer) { result.push({ path: path, line: pointer.key ? pointer.key.line : (pointer.value ? pointer.value.line : 0), column: pointer.key ? pointer.key.column : (pointer.value ? pointer.value.column : 0) }); } }); return result; } /** * Get the applied color given a color name or code * Source: https://stackoverflow.com/questions/6386090/validating-css-color-names/33184805 * @param {string} color * @returns {string | null} returns the color if the input is a valid * color, and returns null otherwise. Example output: * 'rgba(255,0,0,0.7)' or 'rgb(255,0,0)' */ exports.getColorCSS = function (color) { var ele = document.createElement('div'); ele.style.color = color; return ele.style.color.split(/\s+/).join('').toLowerCase() || null; } /** * Test if a string contains a valid color name or code. * @param {string} color * @returns {boolean} returns true if a valid color, false otherwise */ exports.isValidColor = function (color) { return !!exports.getColorCSS(color); } /** * Make a tooltip for a field based on the field's schema. * @param {object} schema JSON schema * @param {string} [locale] Locale code (for example, zh-CN) * @returns {string} Field tooltip, may be empty string if all relevant schema properties are missing */ exports.makeFieldTooltip = function (schema, locale) { if (!schema) { return ''; } var tooltip = ''; if (schema.title) { tooltip += schema.title; } if (schema.description) { if (tooltip.length > 0) { tooltip += '\n'; } tooltip += schema.description; } if (schema.default) { if (tooltip.length > 0) { tooltip += '\n\n'; } tooltip += translate('default', undefined, locale) + '\n'; tooltip += JSON.stringify(schema.default, null, 2); } if (Array.isArray(schema.examples) && schema.examples.length > 0) { if (tooltip.length > 0) { tooltip += '\n\n'; } tooltip += translate('examples', undefined, locale) + '\n'; schema.examples.forEach(function (example, index) { tooltip += JSON.stringify(example, null, 2); if (index !== schema.examples.length - 1) { tooltip += '\n'; } }); } return tooltip; } /** * Get a nested property from an object. * Returns undefined when the property does not exist. * @param {Object} object * @param {string[]} path * @return {*} */ exports.get = function (object, path) { var value = object for (var i = 0; i < path.length && value !== undefined && value !== null; i++) { value = value[path[i]] } return value; } /** * Find a unique name. Suffix the name with ' (copy)', '(copy 2)', etc * until a unique name is found * @param {string} name * @param {Array} existingPropNames Array with existing prop names */ exports.findUniqueName = function(name, existingPropNames) { var strippedName = name.replace(/ \(copy( \d+)?\)$/, '') var validName = strippedName var i = 1 while (existingPropNames.indexOf(validName) !== -1) { var copy = 'copy' + (i > 1 ? (' ' + i) : '') validName = strippedName + ' (' + copy + ')' i++ } return validName }