diff --git a/HISTORY.md b/HISTORY.md index 47a2950..7319dfb 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -3,12 +3,12 @@ https://github.com/josdejong/jsoneditor -## not yet released, version 5.18.0 +## 2018-06-27, version 5.18.0 +- Implemented JMESPath support for advanced filtering, sorting, and + transforming of JSON documents. - Implemented a new option `modalAnchor` to control at which part of the screen the modals are displayed. -- Integrated JMESPath for advanced filtering, sorting, and transforming - of JSON documents. - Fixed #544: JSON Schema errors sometimes not being displayed in the editor. diff --git a/dist/img/jsoneditor-icons.svg b/dist/img/jsoneditor-icons.svg index 9e48bdc..ad25fbc 100644 --- a/dist/img/jsoneditor-icons.svg +++ b/dist/img/jsoneditor-icons.svg @@ -7,7 +7,7 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="216" + width="240" height="144" id="svg4136" version="1.1" @@ -30,7 +30,7 @@ + + + + + + id="g4299" + style="stroke:none"> + x="7.0000048" + y="10.999998" + width="9.9999924" + height="1.9999986" + id="svg_1-1" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0" /> + x="11.000005" + y="7.0000114" + width="1.9999955" + height="9.9999838" + id="svg_1-1-1" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0" /> + + + x="7.0000048" + y="10.999998" + width="9.9999924" + height="1.9999986" + id="svg_1-1-0" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0" /> - - - - - - - - - + x="11.000005" + y="7.0000114" + width="1.9999955" + height="9.9999838" + id="svg_1-1-1-9" + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0" /> + + + + + + x="198" + y="10.999999" + width="7.9999909" + height="1.9999965" + id="svg_1-7-5-3" /> + id="rect4374" + height="1.9999946" + width="11.999995" + y="7.0000005" + x="198" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" /> - - - - - - - - - - - - - - - - - - - - - - + id="rect4376" + height="1.9999995" + width="3.9999928" + y="14.999996" + x="198" + style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0" /> + + + + + + + + + + + + + + + + + + + + + diff --git a/dist/jsoneditor-minimalist.js b/dist/jsoneditor-minimalist.js index 3677335..8434feb 100644 --- a/dist/jsoneditor-minimalist.js +++ b/dist/jsoneditor-minimalist.js @@ -24,8 +24,8 @@ * Copyright (c) 2011-2017 Jos de Jong, http://jsoneditoronline.org * * @author Jos de Jong, - * @version 5.17.1 - * @date 2018-06-03 + * @version 5.18.0 + * @date 2018-06-27 */ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') @@ -94,7 +94,7 @@ return /******/ (function(modules) { // webpackBootstrap } var treemode = __webpack_require__(1); - var textmode = __webpack_require__(17); + var textmode = __webpack_require__(20); var util = __webpack_require__(4); /** @@ -136,7 +136,10 @@ return /******/ (function(modules) { // webpackBootstrap * {function} onTextSelectionChange Callback method, * triggered on text selection change * Only applicable for modes - * 'text' and 'code' + * {HTMLElement} modalAnchor The anchor element to apply an + * overlay and display the modals in a + * centered location. + * Defaults to document.body * @param {Object | undefined} json JSON object */ function JSONEditor (container, options, json) { @@ -504,9 +507,9 @@ return /******/ (function(modules) { // webpackBootstrap var ContextMenu = __webpack_require__(7); var TreePath = __webpack_require__(9); var Node = __webpack_require__(10); - var ModeSwitcher = __webpack_require__(15); + var ModeSwitcher = __webpack_require__(18); var util = __webpack_require__(4); - var autocomplete = __webpack_require__(16); + var autocomplete = __webpack_require__(19); var translate = __webpack_require__(8).translate; var setLanguages = __webpack_require__(8).setLanguages; var setLanguage = __webpack_require__(8).setLanguage; @@ -2230,16 +2233,31 @@ return /******/ (function(modules) { // webpackBootstrap var node = params.node; node.hideChilds(); node.childs = params.oldChilds; - node._updateDomIndexes(); + node.updateDom({updateIndexes: true}); node.showChilds(); }, 'redo': function (params) { var node = params.node; node.hideChilds(); node.childs = params.newChilds; - node._updateDomIndexes(); + node.updateDom({updateIndexes: true}); node.showChilds(); } + }, + + 'transform': { + 'undo': function (params) { + var node = params.node; + node.setValue(params.oldValue); + + // TODO: would be nice to restore the state of the node and childs + }, + 'redo': function (params) { + var node = params.node; + node.setValue(params.newValue); + + // TODO: would be nice to restore the state of the node and childs + } } // TODO: restore the original caret position and selection with each undo @@ -2966,8 +2984,7 @@ return /******/ (function(modules) { // webpackBootstrap exports.getInternetExplorerVersion = function getInternetExplorerVersion() { if (_ieVersion == -1) { var rv = -1; // Return value assumes failure. - if (navigator.appName == 'Microsoft Internet Explorer') - { + 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) { @@ -2986,7 +3003,7 @@ return /******/ (function(modules) { // webpackBootstrap * @returns {boolean} isFirefox */ exports.isFirefox = function isFirefox () { - return (navigator.userAgent.indexOf("Firefox") != -1); + return (typeof navigator !== 'undefined' && navigator.userAgent.indexOf("Firefox") !== -1); }; /** @@ -4588,195 +4605,224 @@ return /******/ (function(modules) { // webpackBootstrap var _locales = ['en', 'pt-BR']; var _defs = { - en: { - 'array': 'Array', - 'auto': 'Auto', - 'appendText': 'Append', - 'appendTitle': 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)', - 'appendSubmenuTitle': 'Select the type of the field to be appended', - 'appendTitleAuto': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)', - 'ascending': 'Ascending', - 'ascendingTitle': 'Sort the childs of this ${type} in ascending order', - 'actionsMenu': 'Click to open the actions menu (Ctrl+M)', - 'collapseAll': 'Collapse all fields', - 'descending': 'Descending', - 'descendingTitle': 'Sort the childs of this ${type} in descending order', - 'drag': 'Drag to move this field (Alt+Shift+Arrows)', - 'duplicateKey': 'duplicate key', - 'duplicateText': 'Duplicate', - 'duplicateTitle': 'Duplicate selected fields (Ctrl+D)', - 'duplicateField': 'Duplicate this field (Ctrl+D)', - 'empty': 'empty', - 'expandAll': 'Expand all fields', - 'expandTitle': 'Click to expand/collapse this field (Ctrl+E). \n' + - 'Ctrl+Click to expand/collapse including all childs.', - 'insert': 'Insert', - 'insertTitle': 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)', - 'insertSub': 'Select the type of the field to be inserted', - 'object': 'Object', - 'ok': 'Ok', - 'redo': 'Redo (Ctrl+Shift+Z)', - 'removeText': 'Remove', - 'removeTitle': 'Remove selected fields (Ctrl+Del)', - 'removeField': 'Remove this field (Ctrl+Del)', - 'selectNode': 'Select a node...', - 'showAll': 'show all', - 'showMore': 'show more', - 'showMoreStatus': 'displaying ${visibleChilds} of ${totalChilds} items.', - 'sort': 'Sort', - 'sortTitle': 'Sort the childs of this ', - 'sortFieldLabel': 'Field:', - 'sortDirectionLabel': 'Direction:', - 'sortFieldTitle': 'Select the nested field by which to sort the array or object', - 'sortAscending': 'Ascending', - 'sortAscendingTitle': 'Sort the selected field in ascending order', - 'sortDescending': 'Descending', - 'sortDescendingTitle': 'Sort the selected field in descending order', - 'string': 'String', - 'type': 'Type', - 'typeTitle': 'Change the type of this field', - 'openUrl': 'Ctrl+Click or Ctrl+Enter to open url in new window', - 'undo': 'Undo last action (Ctrl+Z)', - 'validationCannotMove': 'Cannot move a field into a child of itself', - 'autoType': 'Field type "auto". ' + - 'The field type is automatically determined from the value ' + - 'and can be a string, number, boolean, or null.', - 'objectType': 'Field type "object". ' + - 'An object contains an unordered set of key/value pairs.', - 'arrayType': 'Field type "array". ' + - 'An array contains an ordered collection of values.', - 'stringType': 'Field type "string". ' + - 'Field type is not determined from the value, ' + - 'but always returned as string.' - }, - 'pt-BR': { - 'array': 'Lista', - 'auto': 'Automatico', - 'appendText': 'Adicionar', - 'appendTitle': 'Adicionar novo campo com tipo \'auto\' depois deste campo (Ctrl+Shift+Ins)', - 'appendSubmenuTitle': 'Selecione o tipo do campo a ser adicionado', - 'appendTitleAuto': 'Adicionar novo campo com tipo \'auto\' (Ctrl+Shift+Ins)', - 'ascending': 'Ascendente', - 'ascendingTitle': 'Organizar filhor do tipo ${type} em crescente', - 'actionsMenu': 'Clique para abrir o menu de ações (Ctrl+M)', - 'collapseAll': 'Fechar todos campos', - 'descending': 'Descendente', - 'descendingTitle': 'Organizar o filhos do tipo ${type} em decrescente', - 'duplicateKey': 'chave duplicada', - 'drag': 'Arraste para mover este campo (Alt+Shift+Arrows)', - 'duplicateText': 'Duplicar', - 'duplicateTitle': 'Duplicar campos selecionados (Ctrl+D)', - 'duplicateField': 'Duplicar este campo (Ctrl+D)', - 'empty': 'vazio', - 'expandAll': 'Expandir todos campos', - 'expandTitle': 'Clique para expandir/encolher este campo (Ctrl+E). \n' + - 'Ctrl+Click para expandir/encolher incluindo todos os filhos.', - 'insert': 'Inserir', - 'insertTitle': 'Inserir um novo campo do tipo \'auto\' antes deste campo (Ctrl+Ins)', - 'insertSub': 'Selecionar o tipo de campo a ser inserido', - 'object': 'Objeto', - 'ok': 'Ok', - 'redo': 'Refazer (Ctrl+Shift+Z)', - 'removeText': 'Remover', - 'removeTitle': 'Remover campos selecionados (Ctrl+Del)', - 'removeField': 'Remover este campo (Ctrl+Del)', - // TODO: correctly translate selectNode - 'selectNode': 'Select a node...', - // TODO: correctly translate showAll - 'showAll': 'mostre tudo', - // TODO: correctly translate showMore - 'showMore': 'mostre mais', - // TODO: correctly translate showMoreStatus - 'showMoreStatus': 'exibindo ${visibleChilds} de ${totalChilds} itens.', - 'sort': 'Organizar', - 'sortTitle': 'Organizar os filhos deste ', - // TODO: correctly translate sortFieldLabel - 'sortFieldLabel': 'Field:', - // TODO: correctly translate sortDirectionLabel - 'sortDirectionLabel': 'Direction:', - // TODO: correctly translate sortFieldTitle - 'sortFieldTitle': 'Select the nested field by which to sort the array or object', - // TODO: correctly translate sortAscending - 'sortAscending': 'Ascending', - // TODO: correctly translate sortAscendingTitle - 'sortAscendingTitle': 'Sort the selected field in ascending order', - // TODO: correctly translate sortDescending - 'sortDescending': 'Descending', - // TODO: correctly translate sortDescendingTitle - 'sortDescendingTitle': 'Sort the selected field in descending order', - 'string': 'Texto', - 'type': 'Tipo', - 'typeTitle': 'Mudar o tipo deste campo', - 'openUrl': 'Ctrl+Click ou Ctrl+Enter para abrir link em nova janela', - 'undo': 'Desfazer último ação (Ctrl+Z)', - 'validationCannotMove': 'Não pode mover um campo como filho dele mesmo', - 'autoType': 'Campo do tipo "auto". ' + - 'O tipo do campo é determinao automaticamente a partir do seu valor ' + - 'e pode ser texto, número, verdade/falso ou nulo.', - 'objectType': 'Campo do tipo "objeto". ' + - 'Um objeto contém uma lista de pares com chave e valor.', - 'arrayType': 'Campo do tipo "lista". ' + - 'Uma lista contem uma coleção de valores ordenados.', - 'stringType': 'Campo do tipo "string". ' + - 'Campo do tipo nao é determinado através do seu valor, ' + - 'mas sempre retornara um texto.' - } + en: { + 'array': 'Array', + 'auto': 'Auto', + 'appendText': 'Append', + 'appendTitle': 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)', + 'appendSubmenuTitle': 'Select the type of the field to be appended', + 'appendTitleAuto': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)', + 'ascending': 'Ascending', + 'ascendingTitle': 'Sort the childs of this ${type} in ascending order', + 'actionsMenu': 'Click to open the actions menu (Ctrl+M)', + 'collapseAll': 'Collapse all fields', + 'descending': 'Descending', + 'descendingTitle': 'Sort the childs of this ${type} in descending order', + 'drag': 'Drag to move this field (Alt+Shift+Arrows)', + 'duplicateKey': 'duplicate key', + 'duplicateText': 'Duplicate', + 'duplicateTitle': 'Duplicate selected fields (Ctrl+D)', + 'duplicateField': 'Duplicate this field (Ctrl+D)', + 'empty': 'empty', + 'expandAll': 'Expand all fields', + 'expandTitle': 'Click to expand/collapse this field (Ctrl+E). \n' + + 'Ctrl+Click to expand/collapse including all childs.', + 'insert': 'Insert', + 'insertTitle': 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)', + 'insertSub': 'Select the type of the field to be inserted', + 'object': 'Object', + 'ok': 'Ok', + 'redo': 'Redo (Ctrl+Shift+Z)', + 'removeText': 'Remove', + 'removeTitle': 'Remove selected fields (Ctrl+Del)', + 'removeField': 'Remove this field (Ctrl+Del)', + 'selectNode': 'Select a node...', + 'showAll': 'show all', + 'showMore': 'show more', + 'showMoreStatus': 'displaying ${visibleChilds} of ${totalChilds} items.', + 'sort': 'Sort', + 'sortTitle': 'Sort the childs of this ${type}', + 'sortFieldLabel': 'Field:', + 'sortDirectionLabel': 'Direction:', + 'sortFieldTitle': 'Select the nested field by which to sort the array or object', + 'sortAscending': 'Ascending', + 'sortAscendingTitle': 'Sort the selected field in ascending order', + 'sortDescending': 'Descending', + 'sortDescendingTitle': 'Sort the selected field in descending order', + 'string': 'String', + 'transform': 'Transform', + 'transformTitle': 'Filter, sort, or transform the childs of this ${type}', + 'transformQueryTitle': 'Enter a JMESPath query', + 'transformWizardLabel': 'Wizard', + 'transformWizardFilter': 'Filter', + 'transformWizardSortBy': 'Sort by', + 'transformWizardSelectFields': 'Select fields', + 'transformQueryLabel': 'Query', + 'transformPreviewLabel': 'Preview', + 'type': 'Type', + 'typeTitle': 'Change the type of this field', + 'openUrl': 'Ctrl+Click or Ctrl+Enter to open url in new window', + 'undo': 'Undo last action (Ctrl+Z)', + 'validationCannotMove': 'Cannot move a field into a child of itself', + 'autoType': 'Field type "auto". ' + + 'The field type is automatically determined from the value ' + + 'and can be a string, number, boolean, or null.', + 'objectType': 'Field type "object". ' + + 'An object contains an unordered set of key/value pairs.', + 'arrayType': 'Field type "array". ' + + 'An array contains an ordered collection of values.', + 'stringType': 'Field type "string". ' + + 'Field type is not determined from the value, ' + + 'but always returned as string.' + }, + 'pt-BR': { + 'array': 'Lista', + 'auto': 'Automatico', + 'appendText': 'Adicionar', + 'appendTitle': 'Adicionar novo campo com tipo \'auto\' depois deste campo (Ctrl+Shift+Ins)', + 'appendSubmenuTitle': 'Selecione o tipo do campo a ser adicionado', + 'appendTitleAuto': 'Adicionar novo campo com tipo \'auto\' (Ctrl+Shift+Ins)', + 'ascending': 'Ascendente', + 'ascendingTitle': 'Organizar filhor do tipo ${type} em crescente', + 'actionsMenu': 'Clique para abrir o menu de ações (Ctrl+M)', + 'collapseAll': 'Fechar todos campos', + 'descending': 'Descendente', + 'descendingTitle': 'Organizar o filhos do tipo ${type} em decrescente', + 'duplicateKey': 'chave duplicada', + 'drag': 'Arraste para mover este campo (Alt+Shift+Arrows)', + 'duplicateText': 'Duplicar', + 'duplicateTitle': 'Duplicar campos selecionados (Ctrl+D)', + 'duplicateField': 'Duplicar este campo (Ctrl+D)', + 'empty': 'vazio', + 'expandAll': 'Expandir todos campos', + 'expandTitle': 'Clique para expandir/encolher este campo (Ctrl+E). \n' + + 'Ctrl+Click para expandir/encolher incluindo todos os filhos.', + 'insert': 'Inserir', + 'insertTitle': 'Inserir um novo campo do tipo \'auto\' antes deste campo (Ctrl+Ins)', + 'insertSub': 'Selecionar o tipo de campo a ser inserido', + 'object': 'Objeto', + 'ok': 'Ok', + 'redo': 'Refazer (Ctrl+Shift+Z)', + 'removeText': 'Remover', + 'removeTitle': 'Remover campos selecionados (Ctrl+Del)', + 'removeField': 'Remover este campo (Ctrl+Del)', + // TODO: correctly translate + 'selectNode': 'Select a node...', + // TODO: correctly translate + 'showAll': 'mostre tudo', + // TODO: correctly translate + 'showMore': 'mostre mais', + // TODO: correctly translate + 'showMoreStatus': 'exibindo ${visibleChilds} de ${totalChilds} itens.', + 'sort': 'Organizar', + 'sortTitle': 'Organizar os filhos deste ${type}', + // TODO: correctly translate + 'sortFieldLabel': 'Field:', + // TODO: correctly translate + 'sortDirectionLabel': 'Direction:', + // TODO: correctly translate + 'sortFieldTitle': 'Select the nested field by which to sort the array or object', + // TODO: correctly translate + 'sortAscending': 'Ascending', + // TODO: correctly translate + 'sortAscendingTitle': 'Sort the selected field in ascending order', + // TODO: correctly translate + 'sortDescending': 'Descending', + // TODO: correctly translate + 'sortDescendingTitle': 'Sort the selected field in descending order', + 'string': 'Texto', + // TODO: correctly translate + 'transform': 'Transform', + // TODO: correctly translate + 'transformTitle': 'Filter, sort, or transform the childs of this ${type}', + // TODO: correctly translate + 'transformQueryTitle': 'Enter a JMESPath query', + // TODO: correctly translate + 'transformWizardLabel': 'Wizard', + // TODO: correctly translate + 'transformWizardFilter': 'Filter', + // TODO: correctly translate + 'transformWizardSortBy': 'Sort by', + // TODO: correctly translate + 'transformWizardSelectFields': 'Select fields', + // TODO: correctly translate + 'transformQueryLabel': 'Query', + // TODO: correctly translate + 'transformPreviewLabel': 'Preview', + 'type': 'Tipo', + 'typeTitle': 'Mudar o tipo deste campo', + 'openUrl': 'Ctrl+Click ou Ctrl+Enter para abrir link em nova janela', + 'undo': 'Desfazer último ação (Ctrl+Z)', + 'validationCannotMove': 'Não pode mover um campo como filho dele mesmo', + 'autoType': 'Campo do tipo "auto". ' + + 'O tipo do campo é determinao automaticamente a partir do seu valor ' + + 'e pode ser texto, número, verdade/falso ou nulo.', + 'objectType': 'Campo do tipo "objeto". ' + + 'Um objeto contém uma lista de pares com chave e valor.', + 'arrayType': 'Campo do tipo "lista". ' + + 'Uma lista contem uma coleção de valores ordenados.', + 'stringType': 'Campo do tipo "string". ' + + 'Campo do tipo nao é determinado através do seu valor, ' + + 'mas sempre retornara um texto.' + } }; var _defaultLang = 'en'; var _lang; - var userLang = navigator.language || navigator.userLanguage; + var userLang = typeof navigator !== 'undefined' + ? navigator.language || navigator.userLanguage + : undefined; _lang = _locales.find(function (l) { - return l === userLang; + return l === userLang; }); if (!_lang) { - _lang = _defaultLang; + _lang = _defaultLang; } module.exports = { - // supported locales - _locales: _locales, - _defs: _defs, - _lang: _lang, - setLanguage: function (lang) { - if (!lang) { - return; - } - var langFound = _locales.find(function (l) { - return l === lang; - }); - if (langFound) { - _lang = langFound; - } else { - console.error('Language not found'); - } - }, - setLanguages: function (languages) { - if (!languages) { - return; - } - for (var key in languages) { - var langFound = _locales.find(function (l) { - return l === key; - }); - if (!langFound) { - _locales.push(key); - } - _defs[key] = Object.assign({}, _defs[_defaultLang], _defs[key], languages[key]); - } - }, - translate: function (key, data, lang) { - if (!lang) { - lang = _lang; - } - var text = _defs[lang][key]; - if (data) { - for (key in data) { - text = text.replace('${' + key + '}', data[key]); - } - } - return text || key; + // supported locales + _locales: _locales, + _defs: _defs, + _lang: _lang, + setLanguage: function (lang) { + if (!lang) { + return; } + var langFound = _locales.find(function (l) { + return l === lang; + }); + if (langFound) { + _lang = langFound; + } else { + console.error('Language not found'); + } + }, + setLanguages: function (languages) { + if (!languages) { + return; + } + for (var key in languages) { + var langFound = _locales.find(function (l) { + return l === key; + }); + if (!langFound) { + _locales.push(key); + } + _defs[key] = Object.assign({}, _defs[_defaultLang], _defs[key], languages[key]); + } + }, + translate: function (key, data, lang) { + if (!lang) { + lang = _lang; + } + var text = _defs[lang][key]; + if (data) { + for (key in data) { + text = text.replace('${' + key + '}', data[key]); + } + } + return text || key; + } }; /***/ }, @@ -4900,14 +4946,18 @@ return /******/ (function(modules) { // webpackBootstrap 'use strict'; - var naturalSort = __webpack_require__(11); - var picoModal = __webpack_require__(12); + var jmespath = __webpack_require__(11); + var naturalSort = __webpack_require__(12); var ContextMenu = __webpack_require__(7); var appendNodeFactory = __webpack_require__(13); var showMoreNodeFactory = __webpack_require__(14); + var showSortModal = __webpack_require__(15); + var showTransformModal = __webpack_require__(17); var util = __webpack_require__(4); var translate = __webpack_require__(8).translate; + var DEFAULT_MODAL_ANCHOR = document.body; + /** * @constructor Node * Create a new Node @@ -5115,7 +5165,7 @@ return /******/ (function(modules) { // webpackBootstrap Node.prototype.updateError = function() { var error = this.error; var tdError = this.dom.tdError; - if (error && this.dom && this.dom.tr && !tdError) { + if (error && this.dom && this.dom.tr) { if (!tdError) { tdError = document.createElement('td'); this.dom.tdError = tdError; @@ -5226,17 +5276,16 @@ return /******/ (function(modules) { // webpackBootstrap */ Node.prototype.setValue = function(value, type) { var childValue, child, visible; + var notUpdateDom = false; // first clear all current childs (if any) var childs = this.childs; if (childs) { while (childs.length) { - this.removeChild(childs[0]); + this.removeChild(childs[0], notUpdateDom); } } - // TODO: remove the DOM of this Node - this.type = this._getType(value); // check if type corresponds with the provided type @@ -5262,7 +5311,7 @@ return /******/ (function(modules) { // webpackBootstrap value: childValue }); visible = i < this.MAX_VISIBLE_CHILDS; - this.appendChild(child, visible); + this.appendChild(child, visible, notUpdateDom); } } this.value = ''; @@ -5281,7 +5330,7 @@ return /******/ (function(modules) { // webpackBootstrap value: childValue }); visible = i < this.MAX_VISIBLE_CHILDS; - this.appendChild(child, visible); + this.appendChild(child, visible, notUpdateDom); } i++; } @@ -5299,6 +5348,8 @@ return /******/ (function(modules) { // webpackBootstrap this.value = value; } + this.updateDom({'updateIndexes': true}); + this.previousValue = this.value; }; @@ -5564,9 +5615,12 @@ return /******/ (function(modules) { // webpackBootstrap * Add a new child to the node. * Only applicable when Node value is of type array or object * @param {Node} node - * @param {boolean} [visible] If true, the child will be rendered + * @param {boolean} [visible] If true (default), the child will be rendered + * @param {boolean} [updateDom] If true (default), the DOM of both parent + * node and appended node will be updated + * (child count, indexes) */ - Node.prototype.appendChild = function(node, visible) { + Node.prototype.appendChild = function(node, visible, updateDom) { if (this._hasChilds()) { // adjust the link to the parent node.setParent(this); @@ -5579,10 +5633,10 @@ return /******/ (function(modules) { // webpackBootstrap if (this.expanded && visible !== false) { // insert into the DOM, before the appendRow var newTr = node.getDom(); - var appendTr = this.getAppendDom(); - var table = appendTr ? appendTr.parentNode : undefined; - if (appendTr && table) { - table.insertBefore(newTr, appendTr); + var nextTr = this._getNextTr(); + var table = nextTr ? nextTr.parentNode : undefined; + if (nextTr && table) { + table.insertBefore(newTr, nextTr); } node.showChilds(); @@ -5590,8 +5644,10 @@ return /******/ (function(modules) { // webpackBootstrap this.visibleChilds++; } - this.updateDom({'updateIndexes': true}); - node.updateDom({'recurse': true}); + if (updateDom !== false) { + this.updateDom({'updateIndexes': true}); + node.updateDom({'recurse': true}); + } } }; @@ -5959,61 +6015,23 @@ return /******/ (function(modules) { // webpackBootstrap return false; }; - /** - * Move given node into this node - * @param {Node} node the childNode to be moved - * @param {Node} beforeNode node will be inserted before given - * node. If no beforeNode is given, - * the node is appended at the end - * @private - */ - Node.prototype._move = function(node, beforeNode) { - if (node == beforeNode) { - // nothing to do... - return; - } - - // check if this node is not a child of the node to be moved here - if (node.containsNode(this)) { - throw new Error(translate('validationCannotMove')); - } - - // remove the original node - if (node.parent) { - node.parent.removeChild(node); - } - - // create a clone of the node - var clone = node.clone(); - node.clearDom(); - - // insert or append the node - if (beforeNode) { - this.insertBefore(clone, beforeNode); - } - else { - this.appendChild(clone); - } - - /* TODO: adjust the field name (to prevent equal field names) - if (this.type == 'object') { - } - */ - }; - /** * Remove a child from the node. * Only applicable when Node value is of type array or object * @param {Node} node The child node to be removed; + * @param {boolean} [updateDom] If true (default), the DOM of the parent + * node will be updated (like child count) * @return {Node | undefined} node The removed node on success, * else undefined */ - Node.prototype.removeChild = function(node) { + Node.prototype.removeChild = function(node, updateDom) { if (this.childs) { var index = this.childs.indexOf(node); if (index !== -1) { - this.visibleChilds--; + if (index < this.visibleChilds && this.expanded) { + this.visibleChilds--; + } node.hide(); @@ -6024,7 +6042,9 @@ return /******/ (function(modules) { // webpackBootstrap var removedNode = this.childs.splice(index, 1)[0]; removedNode.parent = null; - this.updateDom({'updateIndexes': true}); + if (updateDom !== false) { + this.updateDom({'updateIndexes': true}); + } return removedNode; } @@ -8056,6 +8076,50 @@ return /******/ (function(modules) { // webpackBootstrap this.showChilds(); }; + /** + * Transform the node given a JMESPath query. + * @param {String} query JMESPath query to apply + * @private + */ + Node.prototype.transform = function (query) { + if (!this._hasChilds()) { + return; + } + + this.hideChilds(); // sorting is faster when the childs are not attached to the dom + + // copy the childs array (the old one will be kept for an undo action + var oldType = this.type; + var oldChilds = this.childs; + this.childs = this.childs.concat(); + + try { + // apply the JMESPath query + var oldValue = this.getValue(); + var newValue = jmespath.search(oldValue, query); + + this.setValue(newValue); + + this.editor._onAction('transform', { + node: this, + oldType: oldType, + newType: this.type, + oldValue: oldValue, + newValue: newValue, + oldChilds: oldChilds, + newChilds: this.childs + // TODO: use oldChilds/newChilds in history or clean it up + }); + + this.showChilds(); + } + catch (err) { + this.showChilds(); + + this.editor._onError(err); + } + }; + /** * Get a nested child given a path with properties * @param {String[]} path @@ -8089,16 +8153,17 @@ return /******/ (function(modules) { // webpackBootstrap }; /** - * Get the paths of the sortable child paths of this node + * Get the child paths of this node + * @param {boolean} [includeObjects=false] If true, object and array paths are returned as well * @return {string[]} */ - Node.prototype.getSortablePaths = function () { + Node.prototype.getPaths = function (includeObjects) { if (this.type === 'array') { if (this.childs.length > 0) { // sort on any of the property paths of nested objects var pathsMap = {}; this.childs.forEach(function (child) { - child._getSortablePaths(pathsMap, ''); + child._getPaths(pathsMap, '', includeObjects); }); return Object.keys(pathsMap).sort(); @@ -8118,30 +8183,29 @@ return /******/ (function(modules) { // webpackBootstrap }; /** - * Get the paths of the sortable child paths of this node + * Get the child paths of this node * @param {Object} pathsMap + * @param {boolean} [includeObjects=false] If true, object and array paths are returned as well * @param {string} rootPath */ - Node.prototype._getSortablePaths = function (pathsMap, rootPath) { - if (this.type === 'array') { - // not sortable + Node.prototype._getPaths = function (pathsMap, rootPath, includeObjects) { + if (includeObjects && (this.type === 'array' || this.type === 'object')) { + pathsMap[rootPath || '.'] = true; } - else if (this.type === 'object') { + + if (this.type === 'object') { this.childs.forEach(function (child) { if (child.type === 'object') { - child._getSortablePaths(pathsMap, rootPath + '.' + child.field); + // recurse + child._getPaths(pathsMap, rootPath + '.' + child.field, includeObjects); } - else if (child.type === 'array') { - // not sortable - } - else { // type === 'auto' or type === 'string' - var path = rootPath + '.' + child.field; - pathsMap[path] = true; + else if (child.type === 'auto' || child.type === 'string') { + pathsMap[rootPath + '.' + child.field] = true; } }); } else { - pathsMap[rootPath + '.'] = true; + pathsMap[rootPath || '.'] = true; } }; @@ -8483,10 +8547,21 @@ return /******/ (function(modules) { // webpackBootstrap if (this._hasChilds()) { items.push({ text: translate('sort'), - title: translate('sortTitle') + this.type, + title: translate('sortTitle', {type: this.type}), className: 'jsoneditor-sort-asc', click: function () { - node._showSortModal() + var anchor = node.editor.options.modalAnchor || DEFAULT_MODAL_ANCHOR; + showSortModal(node, anchor) + } + }); + + items.push({ + text: translate('transform'), + title: translate('transformTitle', {type: this.type}), + className: 'jsoneditor-transform', + click: function () { + var anchor = node.editor.options.modalAnchor || DEFAULT_MODAL_ANCHOR; + showTransformModal(node, anchor) } }); } @@ -8625,114 +8700,6 @@ return /******/ (function(modules) { // webpackBootstrap menu.show(anchor, this.editor.content); }; - /** - * Show advanced sorting modal - * @private - */ - Node.prototype._showSortModal = function () { - var node = this; - - var content = '
' + - '
' + translate('sort') + '
' + - '
' + - '' + - '' + - '' + - ' ' + - ' ' + - '' + - '' + - ' ' + - ' ' + - '' + - '' + - '' + - '' + - '' + - '
' + translate('sortFieldLabel') + ' ' + - '
' + - ' ' + - '
' + - '
' + translate('sortDirectionLabel') + ' ' + - '
' + - '' + - '' + - '
' + - '
' + - ' ' + - '
' + - '
' + - '
'; - - picoModal({ - parent: this.editor.frame, - content: content, - overlayClass: 'jsoneditor-modal-overlay', - modalClass: 'jsoneditor-modal' - }) - .afterCreate(function (modal) { - var form = modal.modalElem().querySelector('form'); - var ok = modal.modalElem().querySelector('#ok'); - var field = modal.modalElem().querySelector('#field'); - var direction = modal.modalElem().querySelector('#direction'); - - var paths = node.getSortablePaths().sort(); - - paths.forEach(function (path) { - var option = document.createElement('option'); - option.text = path; - option.value = path; - field.appendChild(option); - }); - - function setDirection(value) { - direction.value = value; - direction.className = 'jsoneditor-button-group jsoneditor-button-group-value-' + direction.value; - } - - field.value = node.sortedBy ? node.sortedBy.path : paths[0]; - setDirection(node.sortedBy ? node.sortedBy.direction : 'asc'); - - direction.onclick = function (event) { - setDirection(event.target.getAttribute('data-value')); - }; - - ok.onclick = function (event) { - event.preventDefault(); - event.stopPropagation(); - - modal.close(); - - var path = field.value; - var pathArray = (path === '.') ? [] : path.split('.').slice(1); - - node.sortedBy = { - path: path, - direction: direction.value - }; - - node.sort(pathArray, direction.value) - }; - - if (form) { // form is not available when JSONEditor is created inside a form - form.onsubmit = ok.onclick; - } - }) - .afterClose(function (modal) { - modal.destroy(); - }) - .show(); - }; - /** * get the type of a value * @param {*} value @@ -8880,6 +8847,1679 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, /* 11 */ +/***/ function(module, exports, __webpack_require__) { + + (function(exports) { + "use strict"; + + function isArray(obj) { + if (obj !== null) { + return Object.prototype.toString.call(obj) === "[object Array]"; + } else { + return false; + } + } + + function isObject(obj) { + if (obj !== null) { + return Object.prototype.toString.call(obj) === "[object Object]"; + } else { + return false; + } + } + + function strictDeepEqual(first, second) { + // Check the scalar case first. + if (first === second) { + return true; + } + + // Check if they are the same type. + var firstType = Object.prototype.toString.call(first); + if (firstType !== Object.prototype.toString.call(second)) { + return false; + } + // We know that first and second have the same type so we can just check the + // first type from now on. + if (isArray(first) === true) { + // Short circuit if they're not the same length; + if (first.length !== second.length) { + return false; + } + for (var i = 0; i < first.length; i++) { + if (strictDeepEqual(first[i], second[i]) === false) { + return false; + } + } + return true; + } + if (isObject(first) === true) { + // An object is equal if it has the same key/value pairs. + var keysSeen = {}; + for (var key in first) { + if (hasOwnProperty.call(first, key)) { + if (strictDeepEqual(first[key], second[key]) === false) { + return false; + } + keysSeen[key] = true; + } + } + // Now check that there aren't any keys in second that weren't + // in first. + for (var key2 in second) { + if (hasOwnProperty.call(second, key2)) { + if (keysSeen[key2] !== true) { + return false; + } + } + } + return true; + } + return false; + } + + function isFalse(obj) { + // From the spec: + // A false value corresponds to the following values: + // Empty list + // Empty object + // Empty string + // False boolean + // null value + + // First check the scalar values. + if (obj === "" || obj === false || obj === null) { + return true; + } else if (isArray(obj) && obj.length === 0) { + // Check for an empty array. + return true; + } else if (isObject(obj)) { + // Check for an empty object. + for (var key in obj) { + // If there are any keys, then + // the object is not empty so the object + // is not false. + if (obj.hasOwnProperty(key)) { + return false; + } + } + return true; + } else { + return false; + } + } + + function objValues(obj) { + var keys = Object.keys(obj); + var values = []; + for (var i = 0; i < keys.length; i++) { + values.push(obj[keys[i]]); + } + return values; + } + + function merge(a, b) { + var merged = {}; + for (var key in a) { + merged[key] = a[key]; + } + for (var key2 in b) { + merged[key2] = b[key2]; + } + return merged; + } + + var trimLeft; + if (typeof String.prototype.trimLeft === "function") { + trimLeft = function(str) { + return str.trimLeft(); + }; + } else { + trimLeft = function(str) { + return str.match(/^\s*(.*)/)[1]; + }; + } + + // Type constants used to define functions. + var TYPE_NUMBER = 0; + var TYPE_ANY = 1; + var TYPE_STRING = 2; + var TYPE_ARRAY = 3; + var TYPE_OBJECT = 4; + var TYPE_BOOLEAN = 5; + var TYPE_EXPREF = 6; + var TYPE_NULL = 7; + var TYPE_ARRAY_NUMBER = 8; + var TYPE_ARRAY_STRING = 9; + + var TOK_EOF = "EOF"; + var TOK_UNQUOTEDIDENTIFIER = "UnquotedIdentifier"; + var TOK_QUOTEDIDENTIFIER = "QuotedIdentifier"; + var TOK_RBRACKET = "Rbracket"; + var TOK_RPAREN = "Rparen"; + var TOK_COMMA = "Comma"; + var TOK_COLON = "Colon"; + var TOK_RBRACE = "Rbrace"; + var TOK_NUMBER = "Number"; + var TOK_CURRENT = "Current"; + var TOK_EXPREF = "Expref"; + var TOK_PIPE = "Pipe"; + var TOK_OR = "Or"; + var TOK_AND = "And"; + var TOK_EQ = "EQ"; + var TOK_GT = "GT"; + var TOK_LT = "LT"; + var TOK_GTE = "GTE"; + var TOK_LTE = "LTE"; + var TOK_NE = "NE"; + var TOK_FLATTEN = "Flatten"; + var TOK_STAR = "Star"; + var TOK_FILTER = "Filter"; + var TOK_DOT = "Dot"; + var TOK_NOT = "Not"; + var TOK_LBRACE = "Lbrace"; + var TOK_LBRACKET = "Lbracket"; + var TOK_LPAREN= "Lparen"; + var TOK_LITERAL= "Literal"; + + // The "&", "[", "<", ">" tokens + // are not in basicToken because + // there are two token variants + // ("&&", "[?", "<=", ">="). This is specially handled + // below. + + var basicTokens = { + ".": TOK_DOT, + "*": TOK_STAR, + ",": TOK_COMMA, + ":": TOK_COLON, + "{": TOK_LBRACE, + "}": TOK_RBRACE, + "]": TOK_RBRACKET, + "(": TOK_LPAREN, + ")": TOK_RPAREN, + "@": TOK_CURRENT + }; + + var operatorStartToken = { + "<": true, + ">": true, + "=": true, + "!": true + }; + + var skipChars = { + " ": true, + "\t": true, + "\n": true + }; + + + function isAlpha(ch) { + return (ch >= "a" && ch <= "z") || + (ch >= "A" && ch <= "Z") || + ch === "_"; + } + + function isNum(ch) { + return (ch >= "0" && ch <= "9") || + ch === "-"; + } + function isAlphaNum(ch) { + return (ch >= "a" && ch <= "z") || + (ch >= "A" && ch <= "Z") || + (ch >= "0" && ch <= "9") || + ch === "_"; + } + + function Lexer() { + } + Lexer.prototype = { + tokenize: function(stream) { + var tokens = []; + this._current = 0; + var start; + var identifier; + var token; + while (this._current < stream.length) { + if (isAlpha(stream[this._current])) { + start = this._current; + identifier = this._consumeUnquotedIdentifier(stream); + tokens.push({type: TOK_UNQUOTEDIDENTIFIER, + value: identifier, + start: start}); + } else if (basicTokens[stream[this._current]] !== undefined) { + tokens.push({type: basicTokens[stream[this._current]], + value: stream[this._current], + start: this._current}); + this._current++; + } else if (isNum(stream[this._current])) { + token = this._consumeNumber(stream); + tokens.push(token); + } else if (stream[this._current] === "[") { + // No need to increment this._current. This happens + // in _consumeLBracket + token = this._consumeLBracket(stream); + tokens.push(token); + } else if (stream[this._current] === "\"") { + start = this._current; + identifier = this._consumeQuotedIdentifier(stream); + tokens.push({type: TOK_QUOTEDIDENTIFIER, + value: identifier, + start: start}); + } else if (stream[this._current] === "'") { + start = this._current; + identifier = this._consumeRawStringLiteral(stream); + tokens.push({type: TOK_LITERAL, + value: identifier, + start: start}); + } else if (stream[this._current] === "`") { + start = this._current; + var literal = this._consumeLiteral(stream); + tokens.push({type: TOK_LITERAL, + value: literal, + start: start}); + } else if (operatorStartToken[stream[this._current]] !== undefined) { + tokens.push(this._consumeOperator(stream)); + } else if (skipChars[stream[this._current]] !== undefined) { + // Ignore whitespace. + this._current++; + } else if (stream[this._current] === "&") { + start = this._current; + this._current++; + if (stream[this._current] === "&") { + this._current++; + tokens.push({type: TOK_AND, value: "&&", start: start}); + } else { + tokens.push({type: TOK_EXPREF, value: "&", start: start}); + } + } else if (stream[this._current] === "|") { + start = this._current; + this._current++; + if (stream[this._current] === "|") { + this._current++; + tokens.push({type: TOK_OR, value: "||", start: start}); + } else { + tokens.push({type: TOK_PIPE, value: "|", start: start}); + } + } else { + var error = new Error("Unknown character:" + stream[this._current]); + error.name = "LexerError"; + throw error; + } + } + return tokens; + }, + + _consumeUnquotedIdentifier: function(stream) { + var start = this._current; + this._current++; + while (this._current < stream.length && isAlphaNum(stream[this._current])) { + this._current++; + } + return stream.slice(start, this._current); + }, + + _consumeQuotedIdentifier: function(stream) { + var start = this._current; + this._current++; + var maxLength = stream.length; + while (stream[this._current] !== "\"" && this._current < maxLength) { + // You can escape a double quote and you can escape an escape. + var current = this._current; + if (stream[current] === "\\" && (stream[current + 1] === "\\" || + stream[current + 1] === "\"")) { + current += 2; + } else { + current++; + } + this._current = current; + } + this._current++; + return JSON.parse(stream.slice(start, this._current)); + }, + + _consumeRawStringLiteral: function(stream) { + var start = this._current; + this._current++; + var maxLength = stream.length; + while (stream[this._current] !== "'" && this._current < maxLength) { + // You can escape a single quote and you can escape an escape. + var current = this._current; + if (stream[current] === "\\" && (stream[current + 1] === "\\" || + stream[current + 1] === "'")) { + current += 2; + } else { + current++; + } + this._current = current; + } + this._current++; + var literal = stream.slice(start + 1, this._current - 1); + return literal.replace("\\'", "'"); + }, + + _consumeNumber: function(stream) { + var start = this._current; + this._current++; + var maxLength = stream.length; + while (isNum(stream[this._current]) && this._current < maxLength) { + this._current++; + } + var value = parseInt(stream.slice(start, this._current)); + return {type: TOK_NUMBER, value: value, start: start}; + }, + + _consumeLBracket: function(stream) { + var start = this._current; + this._current++; + if (stream[this._current] === "?") { + this._current++; + return {type: TOK_FILTER, value: "[?", start: start}; + } else if (stream[this._current] === "]") { + this._current++; + return {type: TOK_FLATTEN, value: "[]", start: start}; + } else { + return {type: TOK_LBRACKET, value: "[", start: start}; + } + }, + + _consumeOperator: function(stream) { + var start = this._current; + var startingChar = stream[start]; + this._current++; + if (startingChar === "!") { + if (stream[this._current] === "=") { + this._current++; + return {type: TOK_NE, value: "!=", start: start}; + } else { + return {type: TOK_NOT, value: "!", start: start}; + } + } else if (startingChar === "<") { + if (stream[this._current] === "=") { + this._current++; + return {type: TOK_LTE, value: "<=", start: start}; + } else { + return {type: TOK_LT, value: "<", start: start}; + } + } else if (startingChar === ">") { + if (stream[this._current] === "=") { + this._current++; + return {type: TOK_GTE, value: ">=", start: start}; + } else { + return {type: TOK_GT, value: ">", start: start}; + } + } else if (startingChar === "=") { + if (stream[this._current] === "=") { + this._current++; + return {type: TOK_EQ, value: "==", start: start}; + } + } + }, + + _consumeLiteral: function(stream) { + this._current++; + var start = this._current; + var maxLength = stream.length; + var literal; + while(stream[this._current] !== "`" && this._current < maxLength) { + // You can escape a literal char or you can escape the escape. + var current = this._current; + if (stream[current] === "\\" && (stream[current + 1] === "\\" || + stream[current + 1] === "`")) { + current += 2; + } else { + current++; + } + this._current = current; + } + var literalString = trimLeft(stream.slice(start, this._current)); + literalString = literalString.replace("\\`", "`"); + if (this._looksLikeJSON(literalString)) { + literal = JSON.parse(literalString); + } else { + // Try to JSON parse it as "" + literal = JSON.parse("\"" + literalString + "\""); + } + // +1 gets us to the ending "`", +1 to move on to the next char. + this._current++; + return literal; + }, + + _looksLikeJSON: function(literalString) { + var startingChars = "[{\""; + var jsonLiterals = ["true", "false", "null"]; + var numberLooking = "-0123456789"; + + if (literalString === "") { + return false; + } else if (startingChars.indexOf(literalString[0]) >= 0) { + return true; + } else if (jsonLiterals.indexOf(literalString) >= 0) { + return true; + } else if (numberLooking.indexOf(literalString[0]) >= 0) { + try { + JSON.parse(literalString); + return true; + } catch (ex) { + return false; + } + } else { + return false; + } + } + }; + + var bindingPower = {}; + bindingPower[TOK_EOF] = 0; + bindingPower[TOK_UNQUOTEDIDENTIFIER] = 0; + bindingPower[TOK_QUOTEDIDENTIFIER] = 0; + bindingPower[TOK_RBRACKET] = 0; + bindingPower[TOK_RPAREN] = 0; + bindingPower[TOK_COMMA] = 0; + bindingPower[TOK_RBRACE] = 0; + bindingPower[TOK_NUMBER] = 0; + bindingPower[TOK_CURRENT] = 0; + bindingPower[TOK_EXPREF] = 0; + bindingPower[TOK_PIPE] = 1; + bindingPower[TOK_OR] = 2; + bindingPower[TOK_AND] = 3; + bindingPower[TOK_EQ] = 5; + bindingPower[TOK_GT] = 5; + bindingPower[TOK_LT] = 5; + bindingPower[TOK_GTE] = 5; + bindingPower[TOK_LTE] = 5; + bindingPower[TOK_NE] = 5; + bindingPower[TOK_FLATTEN] = 9; + bindingPower[TOK_STAR] = 20; + bindingPower[TOK_FILTER] = 21; + bindingPower[TOK_DOT] = 40; + bindingPower[TOK_NOT] = 45; + bindingPower[TOK_LBRACE] = 50; + bindingPower[TOK_LBRACKET] = 55; + bindingPower[TOK_LPAREN] = 60; + + function Parser() { + } + + Parser.prototype = { + parse: function(expression) { + this._loadTokens(expression); + this.index = 0; + var ast = this.expression(0); + if (this._lookahead(0) !== TOK_EOF) { + var t = this._lookaheadToken(0); + var error = new Error( + "Unexpected token type: " + t.type + ", value: " + t.value); + error.name = "ParserError"; + throw error; + } + return ast; + }, + + _loadTokens: function(expression) { + var lexer = new Lexer(); + var tokens = lexer.tokenize(expression); + tokens.push({type: TOK_EOF, value: "", start: expression.length}); + this.tokens = tokens; + }, + + expression: function(rbp) { + var leftToken = this._lookaheadToken(0); + this._advance(); + var left = this.nud(leftToken); + var currentToken = this._lookahead(0); + while (rbp < bindingPower[currentToken]) { + this._advance(); + left = this.led(currentToken, left); + currentToken = this._lookahead(0); + } + return left; + }, + + _lookahead: function(number) { + return this.tokens[this.index + number].type; + }, + + _lookaheadToken: function(number) { + return this.tokens[this.index + number]; + }, + + _advance: function() { + this.index++; + }, + + nud: function(token) { + var left; + var right; + var expression; + switch (token.type) { + case TOK_LITERAL: + return {type: "Literal", value: token.value}; + case TOK_UNQUOTEDIDENTIFIER: + return {type: "Field", name: token.value}; + case TOK_QUOTEDIDENTIFIER: + var node = {type: "Field", name: token.value}; + if (this._lookahead(0) === TOK_LPAREN) { + throw new Error("Quoted identifier not allowed for function names."); + } else { + return node; + } + break; + case TOK_NOT: + right = this.expression(bindingPower.Not); + return {type: "NotExpression", children: [right]}; + case TOK_STAR: + left = {type: "Identity"}; + right = null; + if (this._lookahead(0) === TOK_RBRACKET) { + // This can happen in a multiselect, + // [a, b, *] + right = {type: "Identity"}; + } else { + right = this._parseProjectionRHS(bindingPower.Star); + } + return {type: "ValueProjection", children: [left, right]}; + case TOK_FILTER: + return this.led(token.type, {type: "Identity"}); + case TOK_LBRACE: + return this._parseMultiselectHash(); + case TOK_FLATTEN: + left = {type: TOK_FLATTEN, children: [{type: "Identity"}]}; + right = this._parseProjectionRHS(bindingPower.Flatten); + return {type: "Projection", children: [left, right]}; + case TOK_LBRACKET: + if (this._lookahead(0) === TOK_NUMBER || this._lookahead(0) === TOK_COLON) { + right = this._parseIndexExpression(); + return this._projectIfSlice({type: "Identity"}, right); + } else if (this._lookahead(0) === TOK_STAR && + this._lookahead(1) === TOK_RBRACKET) { + this._advance(); + this._advance(); + right = this._parseProjectionRHS(bindingPower.Star); + return {type: "Projection", + children: [{type: "Identity"}, right]}; + } else { + return this._parseMultiselectList(); + } + break; + case TOK_CURRENT: + return {type: TOK_CURRENT}; + case TOK_EXPREF: + expression = this.expression(bindingPower.Expref); + return {type: "ExpressionReference", children: [expression]}; + case TOK_LPAREN: + var args = []; + while (this._lookahead(0) !== TOK_RPAREN) { + if (this._lookahead(0) === TOK_CURRENT) { + expression = {type: TOK_CURRENT}; + this._advance(); + } else { + expression = this.expression(0); + } + args.push(expression); + } + this._match(TOK_RPAREN); + return args[0]; + default: + this._errorToken(token); + } + }, + + led: function(tokenName, left) { + var right; + switch(tokenName) { + case TOK_DOT: + var rbp = bindingPower.Dot; + if (this._lookahead(0) !== TOK_STAR) { + right = this._parseDotRHS(rbp); + return {type: "Subexpression", children: [left, right]}; + } else { + // Creating a projection. + this._advance(); + right = this._parseProjectionRHS(rbp); + return {type: "ValueProjection", children: [left, right]}; + } + break; + case TOK_PIPE: + right = this.expression(bindingPower.Pipe); + return {type: TOK_PIPE, children: [left, right]}; + case TOK_OR: + right = this.expression(bindingPower.Or); + return {type: "OrExpression", children: [left, right]}; + case TOK_AND: + right = this.expression(bindingPower.And); + return {type: "AndExpression", children: [left, right]}; + case TOK_LPAREN: + var name = left.name; + var args = []; + var expression, node; + while (this._lookahead(0) !== TOK_RPAREN) { + if (this._lookahead(0) === TOK_CURRENT) { + expression = {type: TOK_CURRENT}; + this._advance(); + } else { + expression = this.expression(0); + } + if (this._lookahead(0) === TOK_COMMA) { + this._match(TOK_COMMA); + } + args.push(expression); + } + this._match(TOK_RPAREN); + node = {type: "Function", name: name, children: args}; + return node; + case TOK_FILTER: + var condition = this.expression(0); + this._match(TOK_RBRACKET); + if (this._lookahead(0) === TOK_FLATTEN) { + right = {type: "Identity"}; + } else { + right = this._parseProjectionRHS(bindingPower.Filter); + } + return {type: "FilterProjection", children: [left, right, condition]}; + case TOK_FLATTEN: + var leftNode = {type: TOK_FLATTEN, children: [left]}; + var rightNode = this._parseProjectionRHS(bindingPower.Flatten); + return {type: "Projection", children: [leftNode, rightNode]}; + case TOK_EQ: + case TOK_NE: + case TOK_GT: + case TOK_GTE: + case TOK_LT: + case TOK_LTE: + return this._parseComparator(left, tokenName); + case TOK_LBRACKET: + var token = this._lookaheadToken(0); + if (token.type === TOK_NUMBER || token.type === TOK_COLON) { + right = this._parseIndexExpression(); + return this._projectIfSlice(left, right); + } else { + this._match(TOK_STAR); + this._match(TOK_RBRACKET); + right = this._parseProjectionRHS(bindingPower.Star); + return {type: "Projection", children: [left, right]}; + } + break; + default: + this._errorToken(this._lookaheadToken(0)); + } + }, + + _match: function(tokenType) { + if (this._lookahead(0) === tokenType) { + this._advance(); + } else { + var t = this._lookaheadToken(0); + var error = new Error("Expected " + tokenType + ", got: " + t.type); + error.name = "ParserError"; + throw error; + } + }, + + _errorToken: function(token) { + var error = new Error("Invalid token (" + + token.type + "): \"" + + token.value + "\""); + error.name = "ParserError"; + throw error; + }, + + + _parseIndexExpression: function() { + if (this._lookahead(0) === TOK_COLON || this._lookahead(1) === TOK_COLON) { + return this._parseSliceExpression(); + } else { + var node = { + type: "Index", + value: this._lookaheadToken(0).value}; + this._advance(); + this._match(TOK_RBRACKET); + return node; + } + }, + + _projectIfSlice: function(left, right) { + var indexExpr = {type: "IndexExpression", children: [left, right]}; + if (right.type === "Slice") { + return { + type: "Projection", + children: [indexExpr, this._parseProjectionRHS(bindingPower.Star)] + }; + } else { + return indexExpr; + } + }, + + _parseSliceExpression: function() { + // [start:end:step] where each part is optional, as well as the last + // colon. + var parts = [null, null, null]; + var index = 0; + var currentToken = this._lookahead(0); + while (currentToken !== TOK_RBRACKET && index < 3) { + if (currentToken === TOK_COLON) { + index++; + this._advance(); + } else if (currentToken === TOK_NUMBER) { + parts[index] = this._lookaheadToken(0).value; + this._advance(); + } else { + var t = this._lookahead(0); + var error = new Error("Syntax error, unexpected token: " + + t.value + "(" + t.type + ")"); + error.name = "Parsererror"; + throw error; + } + currentToken = this._lookahead(0); + } + this._match(TOK_RBRACKET); + return { + type: "Slice", + children: parts + }; + }, + + _parseComparator: function(left, comparator) { + var right = this.expression(bindingPower[comparator]); + return {type: "Comparator", name: comparator, children: [left, right]}; + }, + + _parseDotRHS: function(rbp) { + var lookahead = this._lookahead(0); + var exprTokens = [TOK_UNQUOTEDIDENTIFIER, TOK_QUOTEDIDENTIFIER, TOK_STAR]; + if (exprTokens.indexOf(lookahead) >= 0) { + return this.expression(rbp); + } else if (lookahead === TOK_LBRACKET) { + this._match(TOK_LBRACKET); + return this._parseMultiselectList(); + } else if (lookahead === TOK_LBRACE) { + this._match(TOK_LBRACE); + return this._parseMultiselectHash(); + } + }, + + _parseProjectionRHS: function(rbp) { + var right; + if (bindingPower[this._lookahead(0)] < 10) { + right = {type: "Identity"}; + } else if (this._lookahead(0) === TOK_LBRACKET) { + right = this.expression(rbp); + } else if (this._lookahead(0) === TOK_FILTER) { + right = this.expression(rbp); + } else if (this._lookahead(0) === TOK_DOT) { + this._match(TOK_DOT); + right = this._parseDotRHS(rbp); + } else { + var t = this._lookaheadToken(0); + var error = new Error("Sytanx error, unexpected token: " + + t.value + "(" + t.type + ")"); + error.name = "ParserError"; + throw error; + } + return right; + }, + + _parseMultiselectList: function() { + var expressions = []; + while (this._lookahead(0) !== TOK_RBRACKET) { + var expression = this.expression(0); + expressions.push(expression); + if (this._lookahead(0) === TOK_COMMA) { + this._match(TOK_COMMA); + if (this._lookahead(0) === TOK_RBRACKET) { + throw new Error("Unexpected token Rbracket"); + } + } + } + this._match(TOK_RBRACKET); + return {type: "MultiSelectList", children: expressions}; + }, + + _parseMultiselectHash: function() { + var pairs = []; + var identifierTypes = [TOK_UNQUOTEDIDENTIFIER, TOK_QUOTEDIDENTIFIER]; + var keyToken, keyName, value, node; + for (;;) { + keyToken = this._lookaheadToken(0); + if (identifierTypes.indexOf(keyToken.type) < 0) { + throw new Error("Expecting an identifier token, got: " + + keyToken.type); + } + keyName = keyToken.value; + this._advance(); + this._match(TOK_COLON); + value = this.expression(0); + node = {type: "KeyValuePair", name: keyName, value: value}; + pairs.push(node); + if (this._lookahead(0) === TOK_COMMA) { + this._match(TOK_COMMA); + } else if (this._lookahead(0) === TOK_RBRACE) { + this._match(TOK_RBRACE); + break; + } + } + return {type: "MultiSelectHash", children: pairs}; + } + }; + + + function TreeInterpreter(runtime) { + this.runtime = runtime; + } + + TreeInterpreter.prototype = { + search: function(node, value) { + return this.visit(node, value); + }, + + visit: function(node, value) { + var matched, current, result, first, second, field, left, right, collected, i; + switch (node.type) { + case "Field": + if (value === null ) { + return null; + } else if (isObject(value)) { + field = value[node.name]; + if (field === undefined) { + return null; + } else { + return field; + } + } else { + return null; + } + break; + case "Subexpression": + result = this.visit(node.children[0], value); + for (i = 1; i < node.children.length; i++) { + result = this.visit(node.children[1], result); + if (result === null) { + return null; + } + } + return result; + case "IndexExpression": + left = this.visit(node.children[0], value); + right = this.visit(node.children[1], left); + return right; + case "Index": + if (!isArray(value)) { + return null; + } + var index = node.value; + if (index < 0) { + index = value.length + index; + } + result = value[index]; + if (result === undefined) { + result = null; + } + return result; + case "Slice": + if (!isArray(value)) { + return null; + } + var sliceParams = node.children.slice(0); + var computed = this.computeSliceParams(value.length, sliceParams); + var start = computed[0]; + var stop = computed[1]; + var step = computed[2]; + result = []; + if (step > 0) { + for (i = start; i < stop; i += step) { + result.push(value[i]); + } + } else { + for (i = start; i > stop; i += step) { + result.push(value[i]); + } + } + return result; + case "Projection": + // Evaluate left child. + var base = this.visit(node.children[0], value); + if (!isArray(base)) { + return null; + } + collected = []; + for (i = 0; i < base.length; i++) { + current = this.visit(node.children[1], base[i]); + if (current !== null) { + collected.push(current); + } + } + return collected; + case "ValueProjection": + // Evaluate left child. + base = this.visit(node.children[0], value); + if (!isObject(base)) { + return null; + } + collected = []; + var values = objValues(base); + for (i = 0; i < values.length; i++) { + current = this.visit(node.children[1], values[i]); + if (current !== null) { + collected.push(current); + } + } + return collected; + case "FilterProjection": + base = this.visit(node.children[0], value); + if (!isArray(base)) { + return null; + } + var filtered = []; + var finalResults = []; + for (i = 0; i < base.length; i++) { + matched = this.visit(node.children[2], base[i]); + if (!isFalse(matched)) { + filtered.push(base[i]); + } + } + for (var j = 0; j < filtered.length; j++) { + current = this.visit(node.children[1], filtered[j]); + if (current !== null) { + finalResults.push(current); + } + } + return finalResults; + case "Comparator": + first = this.visit(node.children[0], value); + second = this.visit(node.children[1], value); + switch(node.name) { + case TOK_EQ: + result = strictDeepEqual(first, second); + break; + case TOK_NE: + result = !strictDeepEqual(first, second); + break; + case TOK_GT: + result = first > second; + break; + case TOK_GTE: + result = first >= second; + break; + case TOK_LT: + result = first < second; + break; + case TOK_LTE: + result = first <= second; + break; + default: + throw new Error("Unknown comparator: " + node.name); + } + return result; + case TOK_FLATTEN: + var original = this.visit(node.children[0], value); + if (!isArray(original)) { + return null; + } + var merged = []; + for (i = 0; i < original.length; i++) { + current = original[i]; + if (isArray(current)) { + merged.push.apply(merged, current); + } else { + merged.push(current); + } + } + return merged; + case "Identity": + return value; + case "MultiSelectList": + if (value === null) { + return null; + } + collected = []; + for (i = 0; i < node.children.length; i++) { + collected.push(this.visit(node.children[i], value)); + } + return collected; + case "MultiSelectHash": + if (value === null) { + return null; + } + collected = {}; + var child; + for (i = 0; i < node.children.length; i++) { + child = node.children[i]; + collected[child.name] = this.visit(child.value, value); + } + return collected; + case "OrExpression": + matched = this.visit(node.children[0], value); + if (isFalse(matched)) { + matched = this.visit(node.children[1], value); + } + return matched; + case "AndExpression": + first = this.visit(node.children[0], value); + + if (isFalse(first) === true) { + return first; + } + return this.visit(node.children[1], value); + case "NotExpression": + first = this.visit(node.children[0], value); + return isFalse(first); + case "Literal": + return node.value; + case TOK_PIPE: + left = this.visit(node.children[0], value); + return this.visit(node.children[1], left); + case TOK_CURRENT: + return value; + case "Function": + var resolvedArgs = []; + for (i = 0; i < node.children.length; i++) { + resolvedArgs.push(this.visit(node.children[i], value)); + } + return this.runtime.callFunction(node.name, resolvedArgs); + case "ExpressionReference": + var refNode = node.children[0]; + // Tag the node with a specific attribute so the type + // checker verify the type. + refNode.jmespathType = TOK_EXPREF; + return refNode; + default: + throw new Error("Unknown node type: " + node.type); + } + }, + + computeSliceParams: function(arrayLength, sliceParams) { + var start = sliceParams[0]; + var stop = sliceParams[1]; + var step = sliceParams[2]; + var computed = [null, null, null]; + if (step === null) { + step = 1; + } else if (step === 0) { + var error = new Error("Invalid slice, step cannot be 0"); + error.name = "RuntimeError"; + throw error; + } + var stepValueNegative = step < 0 ? true : false; + + if (start === null) { + start = stepValueNegative ? arrayLength - 1 : 0; + } else { + start = this.capSliceRange(arrayLength, start, step); + } + + if (stop === null) { + stop = stepValueNegative ? -1 : arrayLength; + } else { + stop = this.capSliceRange(arrayLength, stop, step); + } + computed[0] = start; + computed[1] = stop; + computed[2] = step; + return computed; + }, + + capSliceRange: function(arrayLength, actualValue, step) { + if (actualValue < 0) { + actualValue += arrayLength; + if (actualValue < 0) { + actualValue = step < 0 ? -1 : 0; + } + } else if (actualValue >= arrayLength) { + actualValue = step < 0 ? arrayLength - 1 : arrayLength; + } + return actualValue; + } + + }; + + function Runtime(interpreter) { + this._interpreter = interpreter; + this.functionTable = { + // name: [function, ] + // The can be: + // + // { + // args: [[type1, type2], [type1, type2]], + // variadic: true|false + // } + // + // Each arg in the arg list is a list of valid types + // (if the function is overloaded and supports multiple + // types. If the type is "any" then no type checking + // occurs on the argument. Variadic is optional + // and if not provided is assumed to be false. + abs: {_func: this._functionAbs, _signature: [{types: [TYPE_NUMBER]}]}, + avg: {_func: this._functionAvg, _signature: [{types: [TYPE_ARRAY_NUMBER]}]}, + ceil: {_func: this._functionCeil, _signature: [{types: [TYPE_NUMBER]}]}, + contains: { + _func: this._functionContains, + _signature: [{types: [TYPE_STRING, TYPE_ARRAY]}, + {types: [TYPE_ANY]}]}, + "ends_with": { + _func: this._functionEndsWith, + _signature: [{types: [TYPE_STRING]}, {types: [TYPE_STRING]}]}, + floor: {_func: this._functionFloor, _signature: [{types: [TYPE_NUMBER]}]}, + length: { + _func: this._functionLength, + _signature: [{types: [TYPE_STRING, TYPE_ARRAY, TYPE_OBJECT]}]}, + map: { + _func: this._functionMap, + _signature: [{types: [TYPE_EXPREF]}, {types: [TYPE_ARRAY]}]}, + max: { + _func: this._functionMax, + _signature: [{types: [TYPE_ARRAY_NUMBER, TYPE_ARRAY_STRING]}]}, + "merge": { + _func: this._functionMerge, + _signature: [{types: [TYPE_OBJECT], variadic: true}] + }, + "max_by": { + _func: this._functionMaxBy, + _signature: [{types: [TYPE_ARRAY]}, {types: [TYPE_EXPREF]}] + }, + sum: {_func: this._functionSum, _signature: [{types: [TYPE_ARRAY_NUMBER]}]}, + "starts_with": { + _func: this._functionStartsWith, + _signature: [{types: [TYPE_STRING]}, {types: [TYPE_STRING]}]}, + min: { + _func: this._functionMin, + _signature: [{types: [TYPE_ARRAY_NUMBER, TYPE_ARRAY_STRING]}]}, + "min_by": { + _func: this._functionMinBy, + _signature: [{types: [TYPE_ARRAY]}, {types: [TYPE_EXPREF]}] + }, + type: {_func: this._functionType, _signature: [{types: [TYPE_ANY]}]}, + keys: {_func: this._functionKeys, _signature: [{types: [TYPE_OBJECT]}]}, + values: {_func: this._functionValues, _signature: [{types: [TYPE_OBJECT]}]}, + sort: {_func: this._functionSort, _signature: [{types: [TYPE_ARRAY_STRING, TYPE_ARRAY_NUMBER]}]}, + "sort_by": { + _func: this._functionSortBy, + _signature: [{types: [TYPE_ARRAY]}, {types: [TYPE_EXPREF]}] + }, + join: { + _func: this._functionJoin, + _signature: [ + {types: [TYPE_STRING]}, + {types: [TYPE_ARRAY_STRING]} + ] + }, + reverse: { + _func: this._functionReverse, + _signature: [{types: [TYPE_STRING, TYPE_ARRAY]}]}, + "to_array": {_func: this._functionToArray, _signature: [{types: [TYPE_ANY]}]}, + "to_string": {_func: this._functionToString, _signature: [{types: [TYPE_ANY]}]}, + "to_number": {_func: this._functionToNumber, _signature: [{types: [TYPE_ANY]}]}, + "not_null": { + _func: this._functionNotNull, + _signature: [{types: [TYPE_ANY], variadic: true}] + } + }; + } + + Runtime.prototype = { + callFunction: function(name, resolvedArgs) { + var functionEntry = this.functionTable[name]; + if (functionEntry === undefined) { + throw new Error("Unknown function: " + name + "()"); + } + this._validateArgs(name, resolvedArgs, functionEntry._signature); + return functionEntry._func.call(this, resolvedArgs); + }, + + _validateArgs: function(name, args, signature) { + // Validating the args requires validating + // the correct arity and the correct type of each arg. + // If the last argument is declared as variadic, then we need + // a minimum number of args to be required. Otherwise it has to + // be an exact amount. + var pluralized; + if (signature[signature.length - 1].variadic) { + if (args.length < signature.length) { + pluralized = signature.length === 1 ? " argument" : " arguments"; + throw new Error("ArgumentError: " + name + "() " + + "takes at least" + signature.length + pluralized + + " but received " + args.length); + } + } else if (args.length !== signature.length) { + pluralized = signature.length === 1 ? " argument" : " arguments"; + throw new Error("ArgumentError: " + name + "() " + + "takes " + signature.length + pluralized + + " but received " + args.length); + } + var currentSpec; + var actualType; + var typeMatched; + for (var i = 0; i < signature.length; i++) { + typeMatched = false; + currentSpec = signature[i].types; + actualType = this._getTypeName(args[i]); + for (var j = 0; j < currentSpec.length; j++) { + if (this._typeMatches(actualType, currentSpec[j], args[i])) { + typeMatched = true; + break; + } + } + if (!typeMatched) { + throw new Error("TypeError: " + name + "() " + + "expected argument " + (i + 1) + + " to be type " + currentSpec + + " but received type " + actualType + + " instead."); + } + } + }, + + _typeMatches: function(actual, expected, argValue) { + if (expected === TYPE_ANY) { + return true; + } + if (expected === TYPE_ARRAY_STRING || + expected === TYPE_ARRAY_NUMBER || + expected === TYPE_ARRAY) { + // The expected type can either just be array, + // or it can require a specific subtype (array of numbers). + // + // The simplest case is if "array" with no subtype is specified. + if (expected === TYPE_ARRAY) { + return actual === TYPE_ARRAY; + } else if (actual === TYPE_ARRAY) { + // Otherwise we need to check subtypes. + // I think this has potential to be improved. + var subtype; + if (expected === TYPE_ARRAY_NUMBER) { + subtype = TYPE_NUMBER; + } else if (expected === TYPE_ARRAY_STRING) { + subtype = TYPE_STRING; + } + for (var i = 0; i < argValue.length; i++) { + if (!this._typeMatches( + this._getTypeName(argValue[i]), subtype, + argValue[i])) { + return false; + } + } + return true; + } + } else { + return actual === expected; + } + }, + _getTypeName: function(obj) { + switch (Object.prototype.toString.call(obj)) { + case "[object String]": + return TYPE_STRING; + case "[object Number]": + return TYPE_NUMBER; + case "[object Array]": + return TYPE_ARRAY; + case "[object Boolean]": + return TYPE_BOOLEAN; + case "[object Null]": + return TYPE_NULL; + case "[object Object]": + // Check if it's an expref. If it has, it's been + // tagged with a jmespathType attr of 'Expref'; + if (obj.jmespathType === TOK_EXPREF) { + return TYPE_EXPREF; + } else { + return TYPE_OBJECT; + } + } + }, + + _functionStartsWith: function(resolvedArgs) { + return resolvedArgs[0].lastIndexOf(resolvedArgs[1]) === 0; + }, + + _functionEndsWith: function(resolvedArgs) { + var searchStr = resolvedArgs[0]; + var suffix = resolvedArgs[1]; + return searchStr.indexOf(suffix, searchStr.length - suffix.length) !== -1; + }, + + _functionReverse: function(resolvedArgs) { + var typeName = this._getTypeName(resolvedArgs[0]); + if (typeName === TYPE_STRING) { + var originalStr = resolvedArgs[0]; + var reversedStr = ""; + for (var i = originalStr.length - 1; i >= 0; i--) { + reversedStr += originalStr[i]; + } + return reversedStr; + } else { + var reversedArray = resolvedArgs[0].slice(0); + reversedArray.reverse(); + return reversedArray; + } + }, + + _functionAbs: function(resolvedArgs) { + return Math.abs(resolvedArgs[0]); + }, + + _functionCeil: function(resolvedArgs) { + return Math.ceil(resolvedArgs[0]); + }, + + _functionAvg: function(resolvedArgs) { + var sum = 0; + var inputArray = resolvedArgs[0]; + for (var i = 0; i < inputArray.length; i++) { + sum += inputArray[i]; + } + return sum / inputArray.length; + }, + + _functionContains: function(resolvedArgs) { + return resolvedArgs[0].indexOf(resolvedArgs[1]) >= 0; + }, + + _functionFloor: function(resolvedArgs) { + return Math.floor(resolvedArgs[0]); + }, + + _functionLength: function(resolvedArgs) { + if (!isObject(resolvedArgs[0])) { + return resolvedArgs[0].length; + } else { + // As far as I can tell, there's no way to get the length + // of an object without O(n) iteration through the object. + return Object.keys(resolvedArgs[0]).length; + } + }, + + _functionMap: function(resolvedArgs) { + var mapped = []; + var interpreter = this._interpreter; + var exprefNode = resolvedArgs[0]; + var elements = resolvedArgs[1]; + for (var i = 0; i < elements.length; i++) { + mapped.push(interpreter.visit(exprefNode, elements[i])); + } + return mapped; + }, + + _functionMerge: function(resolvedArgs) { + var merged = {}; + for (var i = 0; i < resolvedArgs.length; i++) { + var current = resolvedArgs[i]; + for (var key in current) { + merged[key] = current[key]; + } + } + return merged; + }, + + _functionMax: function(resolvedArgs) { + if (resolvedArgs[0].length > 0) { + var typeName = this._getTypeName(resolvedArgs[0][0]); + if (typeName === TYPE_NUMBER) { + return Math.max.apply(Math, resolvedArgs[0]); + } else { + var elements = resolvedArgs[0]; + var maxElement = elements[0]; + for (var i = 1; i < elements.length; i++) { + if (maxElement.localeCompare(elements[i]) < 0) { + maxElement = elements[i]; + } + } + return maxElement; + } + } else { + return null; + } + }, + + _functionMin: function(resolvedArgs) { + if (resolvedArgs[0].length > 0) { + var typeName = this._getTypeName(resolvedArgs[0][0]); + if (typeName === TYPE_NUMBER) { + return Math.min.apply(Math, resolvedArgs[0]); + } else { + var elements = resolvedArgs[0]; + var minElement = elements[0]; + for (var i = 1; i < elements.length; i++) { + if (elements[i].localeCompare(minElement) < 0) { + minElement = elements[i]; + } + } + return minElement; + } + } else { + return null; + } + }, + + _functionSum: function(resolvedArgs) { + var sum = 0; + var listToSum = resolvedArgs[0]; + for (var i = 0; i < listToSum.length; i++) { + sum += listToSum[i]; + } + return sum; + }, + + _functionType: function(resolvedArgs) { + switch (this._getTypeName(resolvedArgs[0])) { + case TYPE_NUMBER: + return "number"; + case TYPE_STRING: + return "string"; + case TYPE_ARRAY: + return "array"; + case TYPE_OBJECT: + return "object"; + case TYPE_BOOLEAN: + return "boolean"; + case TYPE_EXPREF: + return "expref"; + case TYPE_NULL: + return "null"; + } + }, + + _functionKeys: function(resolvedArgs) { + return Object.keys(resolvedArgs[0]); + }, + + _functionValues: function(resolvedArgs) { + var obj = resolvedArgs[0]; + var keys = Object.keys(obj); + var values = []; + for (var i = 0; i < keys.length; i++) { + values.push(obj[keys[i]]); + } + return values; + }, + + _functionJoin: function(resolvedArgs) { + var joinChar = resolvedArgs[0]; + var listJoin = resolvedArgs[1]; + return listJoin.join(joinChar); + }, + + _functionToArray: function(resolvedArgs) { + if (this._getTypeName(resolvedArgs[0]) === TYPE_ARRAY) { + return resolvedArgs[0]; + } else { + return [resolvedArgs[0]]; + } + }, + + _functionToString: function(resolvedArgs) { + if (this._getTypeName(resolvedArgs[0]) === TYPE_STRING) { + return resolvedArgs[0]; + } else { + return JSON.stringify(resolvedArgs[0]); + } + }, + + _functionToNumber: function(resolvedArgs) { + var typeName = this._getTypeName(resolvedArgs[0]); + var convertedValue; + if (typeName === TYPE_NUMBER) { + return resolvedArgs[0]; + } else if (typeName === TYPE_STRING) { + convertedValue = +resolvedArgs[0]; + if (!isNaN(convertedValue)) { + return convertedValue; + } + } + return null; + }, + + _functionNotNull: function(resolvedArgs) { + for (var i = 0; i < resolvedArgs.length; i++) { + if (this._getTypeName(resolvedArgs[i]) !== TYPE_NULL) { + return resolvedArgs[i]; + } + } + return null; + }, + + _functionSort: function(resolvedArgs) { + var sortedArray = resolvedArgs[0].slice(0); + sortedArray.sort(); + return sortedArray; + }, + + _functionSortBy: function(resolvedArgs) { + var sortedArray = resolvedArgs[0].slice(0); + if (sortedArray.length === 0) { + return sortedArray; + } + var interpreter = this._interpreter; + var exprefNode = resolvedArgs[1]; + var requiredType = this._getTypeName( + interpreter.visit(exprefNode, sortedArray[0])); + if ([TYPE_NUMBER, TYPE_STRING].indexOf(requiredType) < 0) { + throw new Error("TypeError"); + } + var that = this; + // In order to get a stable sort out of an unstable + // sort algorithm, we decorate/sort/undecorate (DSU) + // by creating a new list of [index, element] pairs. + // In the cmp function, if the evaluated elements are + // equal, then the index will be used as the tiebreaker. + // After the decorated list has been sorted, it will be + // undecorated to extract the original elements. + var decorated = []; + for (var i = 0; i < sortedArray.length; i++) { + decorated.push([i, sortedArray[i]]); + } + decorated.sort(function(a, b) { + var exprA = interpreter.visit(exprefNode, a[1]); + var exprB = interpreter.visit(exprefNode, b[1]); + if (that._getTypeName(exprA) !== requiredType) { + throw new Error( + "TypeError: expected " + requiredType + ", received " + + that._getTypeName(exprA)); + } else if (that._getTypeName(exprB) !== requiredType) { + throw new Error( + "TypeError: expected " + requiredType + ", received " + + that._getTypeName(exprB)); + } + if (exprA > exprB) { + return 1; + } else if (exprA < exprB) { + return -1; + } else { + // If they're equal compare the items by their + // order to maintain relative order of equal keys + // (i.e. to get a stable sort). + return a[0] - b[0]; + } + }); + // Undecorate: extract out the original list elements. + for (var j = 0; j < decorated.length; j++) { + sortedArray[j] = decorated[j][1]; + } + return sortedArray; + }, + + _functionMaxBy: function(resolvedArgs) { + var exprefNode = resolvedArgs[1]; + var resolvedArray = resolvedArgs[0]; + var keyFunction = this.createKeyFunction(exprefNode, [TYPE_NUMBER, TYPE_STRING]); + var maxNumber = -Infinity; + var maxRecord; + var current; + for (var i = 0; i < resolvedArray.length; i++) { + current = keyFunction(resolvedArray[i]); + if (current > maxNumber) { + maxNumber = current; + maxRecord = resolvedArray[i]; + } + } + return maxRecord; + }, + + _functionMinBy: function(resolvedArgs) { + var exprefNode = resolvedArgs[1]; + var resolvedArray = resolvedArgs[0]; + var keyFunction = this.createKeyFunction(exprefNode, [TYPE_NUMBER, TYPE_STRING]); + var minNumber = Infinity; + var minRecord; + var current; + for (var i = 0; i < resolvedArray.length; i++) { + current = keyFunction(resolvedArray[i]); + if (current < minNumber) { + minNumber = current; + minRecord = resolvedArray[i]; + } + } + return minRecord; + }, + + createKeyFunction: function(exprefNode, allowedTypes) { + var that = this; + var interpreter = this._interpreter; + var keyFunc = function(x) { + var current = interpreter.visit(exprefNode, x); + if (allowedTypes.indexOf(that._getTypeName(current)) < 0) { + var msg = "TypeError: expected one of " + allowedTypes + + ", received " + that._getTypeName(current); + throw new Error(msg); + } + return current; + }; + return keyFunc; + } + + }; + + function compile(stream) { + var parser = new Parser(); + var ast = parser.parse(stream); + return ast; + } + + function tokenize(stream) { + var lexer = new Lexer(); + return lexer.tokenize(stream); + } + + function search(data, expression) { + var parser = new Parser(); + // This needs to be improved. Both the interpreter and runtime depend on + // each other. The runtime needs the interpreter to support exprefs. + // There's likely a clean way to avoid the cyclic dependency. + var runtime = new Runtime(); + var interpreter = new TreeInterpreter(runtime); + runtime._interpreter = interpreter; + var node = parser.parse(expression); + return interpreter.search(node, data); + } + + exports.tokenize = tokenize; + exports.compile = compile; + exports.search = search; + exports.strictDeepEqual = strictDeepEqual; + })( false ? this.jmespath = {} : exports); + + +/***/ }, +/* 12 */ /***/ function(module, exports) { /* @@ -8930,7 +10570,525 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 12 */ +/* 13 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var util = __webpack_require__(4); + var ContextMenu = __webpack_require__(7); + var translate = __webpack_require__(8).translate; + + /** + * A factory function to create an AppendNode, which depends on a Node + * @param {Node} Node + */ + function appendNodeFactory(Node) { + /** + * @constructor AppendNode + * @extends Node + * @param {TreeEditor} editor + * Create a new AppendNode. This is a special node which is created at the + * end of the list with childs for an object or array + */ + function AppendNode (editor) { + /** @type {TreeEditor} */ + this.editor = editor; + this.dom = {}; + } + + AppendNode.prototype = new Node(); + + /** + * Return a table row with an append button. + * @return {Element} dom TR element + */ + AppendNode.prototype.getDom = function () { + // TODO: implement a new solution for the append node + var dom = this.dom; + + if (dom.tr) { + return dom.tr; + } + + this._updateEditability(); + + // a row for the append button + var trAppend = document.createElement('tr'); + trAppend.className = 'jsoneditor-append'; + trAppend.node = this; + dom.tr = trAppend; + + // TODO: consistent naming + + if (this.editor.options.mode === 'tree') { + // a cell for the dragarea column + dom.tdDrag = document.createElement('td'); + + // create context menu + var tdMenu = document.createElement('td'); + dom.tdMenu = tdMenu; + var menu = document.createElement('button'); + menu.type = 'button'; + menu.className = 'jsoneditor-contextmenu'; + menu.title = 'Click to open the actions menu (Ctrl+M)'; + dom.menu = menu; + tdMenu.appendChild(dom.menu); + } + + // a cell for the contents (showing text 'empty') + var tdAppend = document.createElement('td'); + var domText = document.createElement('div'); + domText.innerHTML = '(' + translate('empty') + ')'; + domText.className = 'jsoneditor-readonly'; + tdAppend.appendChild(domText); + dom.td = tdAppend; + dom.text = domText; + + this.updateDom(); + + return trAppend; + }; + + /** + * Update the HTML dom of the Node + */ + AppendNode.prototype.updateDom = function(options) { + var dom = this.dom; + var tdAppend = dom.td; + if (tdAppend) { + tdAppend.style.paddingLeft = (this.getLevel() * 24 + 26) + 'px'; + // TODO: not so nice hard coded offset + } + + var domText = dom.text; + if (domText) { + domText.innerHTML = '(' + translate('empty') + ' ' + this.parent.type + ')'; + } + + // attach or detach the contents of the append node: + // hide when the parent has childs, show when the parent has no childs + var trAppend = dom.tr; + if (!this.isVisible()) { + if (dom.tr.firstChild) { + if (dom.tdDrag) { + trAppend.removeChild(dom.tdDrag); + } + if (dom.tdMenu) { + trAppend.removeChild(dom.tdMenu); + } + trAppend.removeChild(tdAppend); + } + } + else { + if (!dom.tr.firstChild) { + if (dom.tdDrag) { + trAppend.appendChild(dom.tdDrag); + } + if (dom.tdMenu) { + trAppend.appendChild(dom.tdMenu); + } + trAppend.appendChild(tdAppend); + } + } + }; + + /** + * Check whether the AppendNode is currently visible. + * the AppendNode is visible when its parent has no childs (i.e. is empty). + * @return {boolean} isVisible + */ + AppendNode.prototype.isVisible = function () { + return (this.parent.childs.length == 0); + }; + + /** + * Show a contextmenu for this node + * @param {HTMLElement} anchor The element to attach the menu to. + * @param {function} [onClose] Callback method called when the context menu + * is being closed. + */ + AppendNode.prototype.showContextMenu = function (anchor, onClose) { + var node = this; + var titles = Node.TYPE_TITLES; + var appendSubmenu = [ + { + text: translate('auto'), + className: 'jsoneditor-type-auto', + title: titles.auto, + click: function () { + node._onAppend('', '', 'auto'); + } + }, + { + text: translate('array'), + className: 'jsoneditor-type-array', + title: titles.array, + click: function () { + node._onAppend('', []); + } + }, + { + text: translate('object'), + className: 'jsoneditor-type-object', + title: titles.object, + click: function () { + node._onAppend('', {}); + } + }, + { + text: translate('string'), + className: 'jsoneditor-type-string', + title: titles.string, + click: function () { + node._onAppend('', '', 'string'); + } + } + ]; + node.addTemplates(appendSubmenu, true); + var items = [ + // create append button + { + 'text': translate('appendText'), + 'title': translate('appendTitleAuto'), + 'submenuTitle': translate('appendSubmenuTitle'), + 'className': 'jsoneditor-insert', + 'click': function () { + node._onAppend('', '', 'auto'); + }, + 'submenu': appendSubmenu + } + ]; + + var menu = new ContextMenu(items, {close: onClose}); + menu.show(anchor, this.editor.content); + }; + + /** + * Handle an event. The event is caught centrally by the editor + * @param {Event} event + */ + AppendNode.prototype.onEvent = function (event) { + var type = event.type; + var target = event.target || event.srcElement; + var dom = this.dom; + + // highlight the append nodes parent + var menu = dom.menu; + if (target == menu) { + if (type == 'mouseover') { + this.editor.highlighter.highlight(this.parent); + } + else if (type == 'mouseout') { + this.editor.highlighter.unhighlight(); + } + } + + // context menu events + if (type == 'click' && target == dom.menu) { + var highlighter = this.editor.highlighter; + highlighter.highlight(this.parent); + highlighter.lock(); + util.addClassName(dom.menu, 'jsoneditor-selected'); + this.showContextMenu(dom.menu, function () { + util.removeClassName(dom.menu, 'jsoneditor-selected'); + highlighter.unlock(); + highlighter.unhighlight(); + }); + } + + if (type == 'keydown') { + this.onKeyDown(event); + } + }; + + return AppendNode; + } + + module.exports = appendNodeFactory; + + +/***/ }, +/* 14 */ +/***/ function(module, exports, __webpack_require__) { + + 'use strict'; + + var translate = __webpack_require__(8).translate; + + /** + * A factory function to create an ShowMoreNode, which depends on a Node + * @param {function} Node + */ + function showMoreNodeFactory(Node) { + /** + * @constructor ShowMoreNode + * @extends Node + * @param {TreeEditor} editor + * @param {Node} parent + * Create a new ShowMoreNode. This is a special node which is created + * for arrays or objects having more than 100 items + */ + function ShowMoreNode (editor, parent) { + /** @type {TreeEditor} */ + this.editor = editor; + this.parent = parent; + this.dom = {}; + } + + ShowMoreNode.prototype = new Node(); + + /** + * Return a table row with an append button. + * @return {Element} dom TR element + */ + ShowMoreNode.prototype.getDom = function () { + if (this.dom.tr) { + return this.dom.tr; + } + + this._updateEditability(); + + // display "show more" + if (!this.dom.tr) { + var me = this; + var parent = this.parent; + var showMoreButton = document.createElement('a'); + showMoreButton.appendChild(document.createTextNode(translate('showMore'))); + showMoreButton.href = '#'; + showMoreButton.onclick = function (event) { + // TODO: use callback instead of accessing a method of the parent + parent.visibleChilds = Math.floor(parent.visibleChilds / parent.MAX_VISIBLE_CHILDS + 1) * + parent.MAX_VISIBLE_CHILDS; + me.updateDom(); + parent.showChilds(); + + event.preventDefault(); + return false; + }; + + var showAllButton = document.createElement('a'); + showAllButton.appendChild(document.createTextNode(translate('showAll'))); + showAllButton.href = '#'; + showAllButton.onclick = function (event) { + // TODO: use callback instead of accessing a method of the parent + parent.visibleChilds = Infinity; + me.updateDom(); + parent.showChilds(); + + event.preventDefault(); + return false; + }; + + var moreContents = document.createElement('div'); + var moreText = document.createTextNode(this._getShowMoreText()); + moreContents.className = 'jsoneditor-show-more'; + moreContents.appendChild(moreText); + moreContents.appendChild(showMoreButton); + moreContents.appendChild(document.createTextNode('. ')); + moreContents.appendChild(showAllButton); + moreContents.appendChild(document.createTextNode('. ')); + + var tdContents = document.createElement('td'); + tdContents.appendChild(moreContents); + + var moreTr = document.createElement('tr'); + moreTr.appendChild(document.createElement('td')); + moreTr.appendChild(document.createElement('td')); + moreTr.appendChild(tdContents); + moreTr.className = 'jsoneditor-show-more'; + this.dom.tr = moreTr; + this.dom.moreContents = moreContents; + this.dom.moreText = moreText; + } + + this.updateDom(); + + return this.dom.tr; + }; + + /** + * Update the HTML dom of the Node + */ + ShowMoreNode.prototype.updateDom = function(options) { + if (this.isVisible()) { + // attach to the right child node (the first non-visible child) + this.dom.tr.node = this.parent.childs[this.parent.visibleChilds]; + + if (!this.dom.tr.parentNode) { + var nextTr = this.parent._getNextTr(); + if (nextTr) { + nextTr.parentNode.insertBefore(this.dom.tr, nextTr); + } + } + + // update the counts in the text + this.dom.moreText.nodeValue = this._getShowMoreText(); + + // update left margin + this.dom.moreContents.style.marginLeft = (this.getLevel() + 1) * 24 + 'px'; + } + else { + if (this.dom.tr && this.dom.tr.parentNode) { + this.dom.tr.parentNode.removeChild(this.dom.tr); + } + } + }; + + ShowMoreNode.prototype._getShowMoreText = function() { + return translate('showMoreStatus', { + visibleChilds: this.parent.visibleChilds, + totalChilds: this.parent.childs.length + }) + ' '; + }; + + /** + * Check whether the ShowMoreNode is currently visible. + * the ShowMoreNode is visible when it's parent has more childs than + * the current visibleChilds + * @return {boolean} isVisible + */ + ShowMoreNode.prototype.isVisible = function () { + return this.parent.expanded && this.parent.childs.length > this.parent.visibleChilds; + }; + + /** + * Handle an event. The event is caught centrally by the editor + * @param {Event} event + */ + ShowMoreNode.prototype.onEvent = function (event) { + var type = event.type; + if (type === 'keydown') { + this.onKeyDown(event); + } + }; + + return ShowMoreNode; + } + + module.exports = showMoreNodeFactory; + + +/***/ }, +/* 15 */ +/***/ function(module, exports, __webpack_require__) { + + var picoModal = __webpack_require__(16); + var translate = __webpack_require__(8).translate; + + /** + * Show advanced sorting modal + * @param {Node} node the node to be sorted + * @param {HTMLElement} container The container where to center + * the modal and create an overlay + */ + function showSortModal (node, container) { + var content = '
' + + '
' + translate('sort') + '
' + + '
' + + '' + + '' + + '' + + ' ' + + ' ' + + '' + + '' + + ' ' + + ' ' + + '' + + '' + + '' + + '' + + '' + + '
' + translate('sortFieldLabel') + ' ' + + '
' + + ' ' + + '
' + + '
' + translate('sortDirectionLabel') + ' ' + + '
' + + '' + + '' + + '
' + + '
' + + ' ' + + '
' + + '
' + + '
'; + + picoModal({ + parent: container, + content: content, + overlayClass: 'jsoneditor-modal-overlay', + modalClass: 'jsoneditor-modal' + }) + .afterCreate(function (modal) { + var form = modal.modalElem().querySelector('form'); + var ok = modal.modalElem().querySelector('#ok'); + var field = modal.modalElem().querySelector('#field'); + var direction = modal.modalElem().querySelector('#direction'); + + var paths = node.getPaths().sort(); + + paths.forEach(function (path) { + var option = document.createElement('option'); + option.text = path; + option.value = path; + field.appendChild(option); + }); + + function setDirection(value) { + direction.value = value; + direction.className = 'jsoneditor-button-group jsoneditor-button-group-value-' + direction.value; + } + + field.value = node.sortedBy ? node.sortedBy.path : paths[0]; + setDirection(node.sortedBy ? node.sortedBy.direction : 'asc'); + + direction.onclick = function (event) { + setDirection(event.target.getAttribute('data-value')); + }; + + ok.onclick = function (event) { + event.preventDefault(); + event.stopPropagation(); + + modal.close(); + + var path = field.value; + var pathArray = (path === '.') ? [] : path.split('.').slice(1); + + node.sortedBy = { + path: path, + direction: direction.value + }; + + node.sort(pathArray, direction.value) + }; + + if (form) { // form is not available when JSONEditor is created inside a form + form.onsubmit = ok.onclick; + } + }) + .afterClose(function (modal) { + modal.destroy(); + }) + .show(); + } + + module.exports = showSortModal; + + +/***/ }, +/* 16 */ /***/ function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/** @@ -9539,407 +11697,304 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 13 */ +/* 17 */ /***/ function(module, exports, __webpack_require__) { - 'use strict'; - - var util = __webpack_require__(4); - var ContextMenu = __webpack_require__(7); + var jmespath = __webpack_require__(11); + var picoModal = __webpack_require__(16); var translate = __webpack_require__(8).translate; + var MAX_PREVIEW_LINES = 100; + /** - * A factory function to create an AppendNode, which depends on a Node - * @param {Node} Node + * Show advanced filter and transform modal using JMESPath + * @param {Node} node the node to be transformed + * @param {HTMLElement} container The container where to center + * the modal and create an overlay */ - function appendNodeFactory(Node) { - /** - * @constructor AppendNode - * @extends Node - * @param {TreeEditor} editor - * Create a new AppendNode. This is a special node which is created at the - * end of the list with childs for an object or array - */ - function AppendNode (editor) { - /** @type {TreeEditor} */ - this.editor = editor; - this.dom = {}; - } + function showTransformModal (node, container) { + var value = node.getValue(); - AppendNode.prototype = new Node(); + var content = '